@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,290 @@
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
+ import { parseLocation, type BucketLocation } from './state/bucket';
8
+
9
+ export interface MachineDefinition {
10
+ id: string;
11
+ ssh: string;
12
+ os: 'linux' | 'macOS' | 'windows';
13
+ network?: {
14
+ tailscaleName: string;
15
+ user: string;
16
+ sshPort: number;
17
+ t3Port: number;
18
+ };
19
+ }
20
+ /** The singleton manager: the human's machine. It runs fleet-sync and hosts
21
+ * every singleton service; it is never converged over SSH. */
22
+ export interface ManagerDefinition {
23
+ id: string;
24
+ os: 'macOS';
25
+ network: { tailscaleName: string; user: string };
26
+ neverSleepOnPower: true;
27
+ }
28
+ export interface FleetDefinition {
29
+ version: 3;
30
+ /** The Git repository machines take dev-fleet's code from. */
31
+ source: { repository: string };
32
+ /** Where this fleet keeps its secrets. */
33
+ secrets: SecretsConfig;
34
+ /** The bucket that holds the fleet state. Its credentials are never part
35
+ * of the configuration: they come from the environment of the command.
36
+ */
37
+ state?: BucketLocation;
38
+ manager: ManagerDefinition;
39
+ /** Stateless, replaceable Linux boxes; clients of the manager's services. */
40
+ workers: MachineDefinition[];
41
+ services: {
42
+ /** Public HTTPS origin of the CLI proxy every node uses. */
43
+ proxy: { endpoint: string };
44
+ monitoring: {
45
+ hubPort: 8443;
46
+ /** Public dashboard origin. */
47
+ publicUrl: string;
48
+ /** Beszel account the manager enrolls nodes with. */
49
+ managerAccount: string;
50
+ /** Read-only Beszel account visitors are signed in as. */
51
+ viewerAccount: string;
52
+ };
53
+ };
54
+ worker?: WorkerPolicy;
55
+ ubuntu: { release: '26.04'; architectures: ['amd64', 'arm64'] };
56
+ }
57
+ const ID = /^[a-z][a-z0-9-]{0,62}$/;
58
+ const HOST = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,252}$/;
59
+ function fail(): never {
60
+ throw new FleetUsageError(
61
+ 'Invalid Fleet definition. Check version 3, one macOS manager, unique Linux worker IDs, singleton services and network metadata in fleet.json.'
62
+ );
63
+ }
64
+ function keys(value: Record<string, unknown>, allowed: string[]): boolean {
65
+ return Object.keys(value).every((key) => allowed.includes(key));
66
+ }
67
+ function network(value: unknown): NonNullable<MachineDefinition['network']> {
68
+ if (
69
+ !record(value) ||
70
+ !keys(value, ['tailscaleName', 'user', 'sshPort', 't3Port'])
71
+ )
72
+ return fail();
73
+ if (
74
+ typeof value['tailscaleName'] !== 'string' ||
75
+ !HOST.test(value['tailscaleName'])
76
+ )
77
+ return fail();
78
+ if (
79
+ typeof value['user'] !== 'string' ||
80
+ !/^[a-z_][a-z0-9_-]{0,31}$/.test(value['user'])
81
+ )
82
+ return fail();
83
+ for (const port of ['sshPort', 't3Port'])
84
+ if (!validPort(value[port])) return fail();
85
+ return value as NonNullable<MachineDefinition['network']>;
86
+ }
87
+ function validPort(value: unknown): boolean {
88
+ return (
89
+ Number.isInteger(value) && Number(value) >= 1 && Number(value) <= 65535
90
+ );
91
+ }
92
+ function worker(value: unknown): MachineDefinition {
93
+ if (!record(value) || !keys(value, ['id', 'ssh', 'os', 'network']))
94
+ return fail();
95
+ if (typeof value['id'] !== 'string' || !ID.test(value['id'])) return fail();
96
+ if (typeof value['ssh'] !== 'string' || !HOST.test(value['ssh']))
97
+ return fail();
98
+ // Workers are the Linux boxes; any other platform is not a worker.
99
+ if (value['os'] !== 'linux') return fail();
100
+ return {
101
+ id: value['id'],
102
+ ssh: value['ssh'],
103
+ os: 'linux',
104
+ ...(value['network'] ? { network: network(value['network']) } : {}),
105
+ };
106
+ }
107
+ function manager(value: unknown): ManagerDefinition {
108
+ if (
109
+ !record(value) ||
110
+ !keys(value, ['id', 'os', 'network', 'neverSleepOnPower']) ||
111
+ typeof value['id'] !== 'string' ||
112
+ !ID.test(value['id']) ||
113
+ value['os'] !== 'macOS' ||
114
+ value['neverSleepOnPower'] !== true
115
+ )
116
+ return fail();
117
+ return {
118
+ id: value['id'],
119
+ os: 'macOS',
120
+ network: managerNetwork(value['network']),
121
+ neverSleepOnPower: true,
122
+ };
123
+ }
124
+ function managerNetwork(value: unknown): ManagerDefinition['network'] {
125
+ if (
126
+ !record(value) ||
127
+ !keys(value, ['tailscaleName', 'user']) ||
128
+ typeof value['tailscaleName'] !== 'string' ||
129
+ !HOST.test(value['tailscaleName']) ||
130
+ typeof value['user'] !== 'string' ||
131
+ !/^[a-z_][a-z0-9_.-]{0,31}$/.test(value['user'])
132
+ )
133
+ return fail();
134
+ return { tailscaleName: value['tailscaleName'], user: value['user'] };
135
+ }
136
+ export function parseDefinition(value: unknown): FleetDefinition {
137
+ if (
138
+ !record(value) ||
139
+ value['version'] !== 3 ||
140
+ !keys(value, [
141
+ '$schema',
142
+ 'version',
143
+ 'source',
144
+ 'secrets',
145
+ 'state',
146
+ 'manager',
147
+ 'workers',
148
+ 'services',
149
+ 'ubuntu',
150
+ 'worker',
151
+ ])
152
+ )
153
+ return fail();
154
+ if (!Array.isArray(value['workers']) || value['workers'].length > 64)
155
+ return fail();
156
+ const lead = manager(value['manager']);
157
+ const workers = value['workers'].map(worker);
158
+ const ids = [lead.id, ...workers.map((entry) => entry.id)];
159
+ if (new Set(ids).size !== ids.length) return fail();
160
+ return {
161
+ version: 3,
162
+ source: source(value['source']),
163
+ secrets: secrets(value['secrets']),
164
+ ...(value['state'] === undefined ? {} : { state: state(value['state']) }),
165
+ manager: lead,
166
+ workers,
167
+ services: services(value['services']),
168
+ ubuntu: baseline(value['ubuntu']),
169
+ ...(value['worker'] === undefined
170
+ ? {}
171
+ : { worker: parseWorkerPolicy(value['worker']) }),
172
+ };
173
+ }
174
+ /** Singleton services name no host: they always run on the manager. */
175
+ function services(value: unknown): FleetDefinition['services'] {
176
+ if (!record(value) || !keys(value, ['proxy', 'monitoring'])) return fail();
177
+ const { proxy, monitoring } = value;
178
+ if (
179
+ !record(proxy) ||
180
+ !keys(proxy, ['endpoint']) ||
181
+ !origin(proxy['endpoint'])
182
+ )
183
+ return fail();
184
+ return {
185
+ proxy: { endpoint: proxy['endpoint'] },
186
+ monitoring: monitoringService(monitoring),
187
+ };
188
+ }
189
+ function monitoringService(
190
+ value: unknown
191
+ ): FleetDefinition['services']['monitoring'] {
192
+ const fields = ['hubPort', 'publicUrl', 'managerAccount', 'viewerAccount'];
193
+ if (!record(value) || !keys(value, fields) || value['hubPort'] !== 8443)
194
+ return fail();
195
+ const { publicUrl, managerAccount, viewerAccount } = value;
196
+ if (!origin(publicUrl) || !account(managerAccount) || !account(viewerAccount))
197
+ return fail();
198
+ return { hubPort: 8443, publicUrl, managerAccount, viewerAccount };
199
+ }
200
+ const origin = (value: unknown): value is string =>
201
+ typeof value === 'string' && /^https:\/\/[A-Za-z0-9.-]{1,253}$/.test(value);
202
+ const account = (value: unknown): value is string =>
203
+ typeof value === 'string' &&
204
+ /^[^\s@]{1,64}@[A-Za-z0-9.-]{1,253}$/.test(value);
205
+ function secrets(value: unknown): SecretsConfig {
206
+ return parseSecretsConfig(value) ?? fail();
207
+ }
208
+ /** Says what is wrong with the block itself: a bucket has several settings. */
209
+ function state(value: unknown): BucketLocation {
210
+ const bucket = parseLocation(value);
211
+ if (bucket.kind === 'directory') return { directory: bucket.path };
212
+ const { bucket: name, endpoint, region } = bucket;
213
+ return {
214
+ bucket: name,
215
+ ...(endpoint ? { endpoint } : {}),
216
+ ...(region ? { region } : {}),
217
+ prefix: bucket.prefix,
218
+ };
219
+ }
220
+ function source(value: unknown): FleetDefinition['source'] {
221
+ if (
222
+ !record(value) ||
223
+ !keys(value, ['repository']) ||
224
+ typeof value['repository'] !== 'string' ||
225
+ !/^https:\/\/[A-Za-z0-9.-]+\/[A-Za-z0-9._/-]+\.git$/.test(
226
+ value['repository']
227
+ )
228
+ )
229
+ return fail();
230
+ return { repository: value['repository'] };
231
+ }
232
+ function baseline(ubuntu: unknown): FleetDefinition['ubuntu'] {
233
+ if (
234
+ !record(ubuntu) ||
235
+ !keys(ubuntu, ['release', 'architectures']) ||
236
+ ubuntu['release'] !== '26.04' ||
237
+ JSON.stringify(ubuntu['architectures']) !== '["amd64","arm64"]'
238
+ )
239
+ return fail();
240
+ return ubuntu as FleetDefinition['ubuntu'];
241
+ }
242
+
243
+ /** The configuration as every machine keeps it at
244
+ * ~/.config/dev-fleet/fleet.json: without the bucket. Only the devfleet CLI
245
+ * on the manager reaches the bucket, and a launcher that still runs the
246
+ * revision from before the block existed must go on accepting the file while
247
+ * a sync is under way.
248
+ */
249
+ export function installedDefinition(definition: FleetDefinition): string {
250
+ const { state: _bucket, ...machine } = definition;
251
+ return JSON.stringify(machine, null, 2) + '\n';
252
+ }
253
+
254
+ export function loadDefinition(path: string): FleetDefinition {
255
+ try {
256
+ return parseDefinition(JSON.parse(readFileSync(path, 'utf8')));
257
+ } catch {
258
+ return fail();
259
+ }
260
+ }
261
+ /** Local v1 roster supplies paths/requirements only for explicitly enrolled workers. */
262
+ export function enrolledRoster(
263
+ definition: FleetDefinition,
264
+ overrides: unknown
265
+ ): FleetConfig {
266
+ const roster = parseConfig(overrides);
267
+ for (const node of roster.nodes) {
268
+ const declared = definition.workers.find((entry) => entry.id === node.id);
269
+ if (!declared)
270
+ throw new FleetUsageError(
271
+ '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.'
272
+ );
273
+ if (declared.ssh !== node.ssh)
274
+ throw new FleetUsageError(
275
+ 'Fleet SSH assignment conflicts with the local roster. Review the migration; discovery cannot overwrite explicit assignments.'
276
+ );
277
+ }
278
+ return {
279
+ ...roster,
280
+ nodes: roster.nodes.map((node) => ({
281
+ ...node,
282
+ required: [
283
+ ...new Set([
284
+ ...node.required,
285
+ ...(definition.worker?.requiredCapabilities ?? []),
286
+ ]),
287
+ ],
288
+ })),
289
+ };
290
+ }
@@ -0,0 +1,92 @@
1
+ import type { Check } from './types';
2
+
3
+ export interface Diagnostic {
4
+ node: string;
5
+ phase: string;
6
+ checkId: string;
7
+ status: Check['status'];
8
+ code: string;
9
+ summary: string;
10
+ expected: string;
11
+ observed: string;
12
+ scope: 'manager' | 'node' | 'unverified';
13
+ durationMs: number | null;
14
+ retryable: boolean;
15
+ actionRequired: boolean;
16
+ remedy: { owner: string; action: string };
17
+ }
18
+ const EXPECTED: Record<string, string> = {
19
+ ssh: 'Fresh noninteractive SSH session with the enrolled host key and designated user.',
20
+ tailscale:
21
+ 'The enrolled device identity is connected and reachable through the intended tailnet.',
22
+ ubuntu:
23
+ 'Observed Ubuntu configuration matches the reviewed baseline without a distribution upgrade.',
24
+ t3: 'Pinned native T3 service and the specifically named runtime acceptance check pass.',
25
+ provider:
26
+ 'The declared gateway model passes authenticated inference, completed streaming and a bounded tool round trip.',
27
+ proxy:
28
+ 'One declared gateway owner preserves runtime identities and serves authorized clients.',
29
+ beszel:
30
+ 'Pinned monitoring artifacts, canonical registration and fresh authenticated metrics are independently verified.',
31
+ rollout: 'Required prerequisites and canary gates pass before promotion.',
32
+ manager:
33
+ 'Manager enrollment, approval and exclusive operation ownership are valid.',
34
+ source:
35
+ 'The approved full commit and packaged canonical definition match verified source.',
36
+ configuration:
37
+ 'The existing Workstation owner applies and verifies the selected source configuration.',
38
+ };
39
+
40
+ /** Derived only from already-redacted checks. No raw subprocess errors, journal
41
+ * contents, auth payloads or environment fields enter the diagnostic contract.
42
+ */
43
+ export function diagnostic(node: string, check: Check): Diagnostic {
44
+ const phase = check.id.split(':')[0]!;
45
+ const failed = check.status !== 'pass';
46
+ const code = /^[A-Z][A-Z0-9_]{2,100}$/.test(check.summary)
47
+ ? check.summary
48
+ : `${check.id.replaceAll(/[^a-z0-9]/g, '_').toUpperCase()}_${failed ? 'UNVERIFIED' : 'VERIFIED'}`;
49
+ return {
50
+ node,
51
+ phase,
52
+ checkId: check.id,
53
+ status: check.status,
54
+ code,
55
+ summary: check.summary,
56
+ expected:
57
+ EXPECTED[phase] ??
58
+ 'The named required check has independent verification evidence.',
59
+ observed: check.summary,
60
+ scope: scope(check.id),
61
+ durationMs: check.phaseDurationMs ?? null,
62
+ retryable: failed && /OFFLINE|UNREACHABLE|METRICS_STALE|TIMEOUT/.test(code),
63
+ actionRequired: failed,
64
+ remedy: {
65
+ owner: owner(phase),
66
+ action:
67
+ check.remedy?.action ??
68
+ 'Inspect this named check in the Fleet runbook; preserve runtime identity and repair persistent configuration in source.',
69
+ },
70
+ };
71
+ }
72
+ function scope(id: string): Diagnostic['scope'] {
73
+ if (
74
+ /^(t3:manager-(endpoint|origin)|ssh:|manager:|rollout:|source:|tailscale:(reachability|identity|enrolled-identity))/.test(
75
+ id
76
+ )
77
+ )
78
+ return 'manager';
79
+ if (
80
+ /^(provider:|proxy:|configuration:|ubuntu:baseline|t3:(service|policy)|tailscale:configuration)/.test(
81
+ id
82
+ )
83
+ )
84
+ return 'node';
85
+ if (/^beszel:(version|registration|connection|freshness|api)/.test(id))
86
+ return 'manager';
87
+ return 'unverified';
88
+ }
89
+ function owner(phase: string): string {
90
+ if (phase === 'proxy') return 'cliproxy';
91
+ return 'dev-fleet';
92
+ }
@@ -0,0 +1,10 @@
1
+ export type CheckSeverity = 'pass' | 'warn' | 'fail';
2
+
3
+ /** One human-readable health finding, with an optional fix to run. */
4
+ export type DoctorCheck = {
5
+ id: string;
6
+ label: string;
7
+ severity: CheckSeverity;
8
+ detail: string;
9
+ fix?: string | undefined;
10
+ };
package/src/errors.ts ADDED
@@ -0,0 +1 @@
1
+ export class FleetUsageError extends Error {}
@@ -0,0 +1,120 @@
1
+ import { readFile, mkdir, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { record } from './config';
4
+ import {
5
+ fileHash,
6
+ manifestHash,
7
+ sidecarHash,
8
+ INPUT_FILES,
9
+ FINGERPRINT_FILES,
10
+ } from './input-hashes';
11
+ import type { Fingerprint, Runner, Check } from './types';
12
+
13
+ const RECEIPT = 'node_modules/.fleet-sync.json';
14
+
15
+ export async function fingerprint(
16
+ root: string,
17
+ run: Runner
18
+ ): Promise<Fingerprint> {
19
+ const files: Fingerprint['files'] = {};
20
+ for (const name of INPUT_FILES)
21
+ files[name] = await fileHash(join(root, name));
22
+ files['workspace-manifests'] = await manifestHash(root);
23
+ files['mise-sidecars'] = await sidecarHash(root);
24
+ const head = await run(['git', 'rev-parse', 'HEAD'], { cwd: root });
25
+ return {
26
+ head:
27
+ head.ok && /^[a-f0-9]{40,64}\s*$/.test(head.stdout)
28
+ ? head.stdout.trim()
29
+ : null,
30
+ files,
31
+ };
32
+ }
33
+
34
+ export async function dependencyCheck(
35
+ root: string,
36
+ current: Fingerprint
37
+ ): Promise<Check> {
38
+ const remedy = {
39
+ action:
40
+ 'Review devfleet fleet-repair --repair workspace, then explicitly apply a frozen toolchain/dependency sync.',
41
+ };
42
+ try {
43
+ const receipt: unknown = JSON.parse(
44
+ await readFile(join(root, RECEIPT), 'utf8')
45
+ );
46
+ const matches =
47
+ record(receipt) &&
48
+ receipt['version'] === 1 &&
49
+ JSON.stringify(receipt['files']) === JSON.stringify(current.files);
50
+ return {
51
+ id: 'dependencies',
52
+ status: matches ? 'pass' : 'fail',
53
+ summary: matches
54
+ ? 'Last successful frozen sync matches input hashes; not a full integrity audit.'
55
+ : 'Dependency sync receipt is stale or invalid.',
56
+ ...(matches ? {} : { remedy }),
57
+ };
58
+ } catch {
59
+ return {
60
+ id: 'dependencies',
61
+ status: 'warn',
62
+ summary: 'No frozen-sync receipt; installed dependencies are unverified.',
63
+ remedy,
64
+ };
65
+ }
66
+ }
67
+
68
+ export async function writeReceipt(
69
+ root: string,
70
+ current: Fingerprint
71
+ ): Promise<void> {
72
+ await mkdir(join(root, 'node_modules'), { recursive: true });
73
+ await writeFile(
74
+ join(root, RECEIPT),
75
+ JSON.stringify({ version: 1, files: current.files }),
76
+ { mode: 0o600 }
77
+ );
78
+ }
79
+
80
+ export function compareFingerprints(
81
+ expected: Fingerprint,
82
+ actual: Fingerprint
83
+ ): Check[] {
84
+ return [
85
+ compare('revision', expected.head, actual.head),
86
+ ...FINGERPRINT_FILES.map((name) =>
87
+ compare(
88
+ `sync:${name}`,
89
+ expected.files[name] ?? null,
90
+ actual.files[name] ?? null
91
+ )
92
+ ),
93
+ ];
94
+ }
95
+
96
+ function compare(
97
+ id: string,
98
+ expected: string | null,
99
+ actual: string | null
100
+ ): Check {
101
+ const known = expected !== null && actual !== null;
102
+ const matches = known && expected === actual;
103
+ return {
104
+ id,
105
+ status: matches ? 'pass' : 'fail',
106
+ summary: matches
107
+ ? 'Matches the manager checkout.'
108
+ : known
109
+ ? 'Differs from the manager checkout.'
110
+ : 'Cannot verify; reference or node input is missing.',
111
+ ...(matches
112
+ ? {}
113
+ : {
114
+ remedy: {
115
+ action:
116
+ 'Reconcile branches/lockfiles; review devfleet fleet-sync for the approved revision. Never reset a working tree.',
117
+ },
118
+ }),
119
+ };
120
+ }
@@ -0,0 +1,174 @@
1
+ import { spawn } from 'node:child_process';
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ renameSync,
7
+ writeFileSync,
8
+ } from 'node:fs';
9
+ import { homedir } from 'node:os';
10
+ import { dirname, join } from 'node:path';
11
+ import {
12
+ installedDefinition,
13
+ parseDefinition,
14
+ type FleetDefinition,
15
+ } from './definition';
16
+ import { FleetUsageError } from './errors';
17
+ import { pinnedPath } from './agents/toolchain';
18
+ import type { Runner } from './types';
19
+ import {
20
+ secretsProvider,
21
+ type SecretCommand,
22
+ type SecretsProvider,
23
+ } from './secrets';
24
+ import { isOperator, stateDefinition } from './state/operator';
25
+ import type { BucketLocation } from './state/bucket';
26
+ import { definitionPath, stateRoot } from './state/paths';
27
+
28
+ /** Where every machine keeps the fleet configuration fleet-sync approved.
29
+ * Launchers and scheduled jobs get no environment and read it from here.
30
+ */
31
+ const installedPath = (home: string): string =>
32
+ join(home, '.config/dev-fleet/fleet.json');
33
+
34
+ /** Where this process reads the fleet configuration from, in this order:
35
+ * - `file`: the file `DEVFLEET_CONFIG` names. The devfleet CLI sets it to the
36
+ * `fleet.json` of the project it is run from (src/cli/project.ts);
37
+ * - `state`: the `definition` document in the manager's fleet state, the
38
+ * configuration fleet-sync last applied, which only the devfleet CLI
39
+ * reads. `path` is its copy as a file;
40
+ * - `installed`: the copy fleet-sync installed on this machine.
41
+ */
42
+ export interface DefinitionSource {
43
+ kind: 'file' | 'state' | 'installed';
44
+ path: string;
45
+ }
46
+
47
+ export function definitionSource(
48
+ env = process.env,
49
+ home = homedir()
50
+ ): DefinitionSource {
51
+ const root = stateRoot(env, home);
52
+ const copy = definitionPath(root);
53
+ const named = env['DEVFLEET_CONFIG'];
54
+ // A devfleet command names the state's copy for the commands it starts.
55
+ const inherited = named === copy && isOperator();
56
+ if (named && !inherited) return { kind: 'file', path: named };
57
+ if (stateDefinition(root) !== null) return { kind: 'state', path: copy };
58
+ return { kind: 'installed', path: installedPath(home) };
59
+ }
60
+
61
+ function writePrivate(path: string, content: string): boolean {
62
+ if (existsSync(path) && readFileSync(path, 'utf8') === content) return false;
63
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
64
+ writeFileSync(`${path}.new`, content, { mode: 0o600 });
65
+ renameSync(`${path}.new`, path);
66
+ return true;
67
+ }
68
+
69
+ /** The file that holds the configuration this process runs with, for the
70
+ * subprocesses that take a path. Every command this process starts reads the
71
+ * same one.
72
+ */
73
+ export function configPath(env = process.env, home = homedir()): string {
74
+ const source = definitionSource(env, home);
75
+ if (source.kind !== 'state') return source.path;
76
+ const body = stateDefinition(stateRoot(env, home));
77
+ if (body !== null) writePrivate(source.path, body);
78
+ env['DEVFLEET_CONFIG'] = source.path;
79
+ return source.path;
80
+ }
81
+
82
+ /** Installs the configuration this process runs with as the machine's copy,
83
+ * so launchers and scheduled jobs (which get no environment) use the same one.
84
+ * Returns whether the file changed.
85
+ */
86
+ export function installConfig(home = homedir()): boolean {
87
+ return writePrivate(installedPath(home), installedDefinition(fleet));
88
+ }
89
+
90
+ function read(path: string): FleetDefinition {
91
+ let value: unknown;
92
+ try {
93
+ value = JSON.parse(readFileSync(path, 'utf8'));
94
+ } catch {
95
+ throw new FleetUsageError(
96
+ `The fleet configuration at ${path} is not readable JSON.`
97
+ );
98
+ }
99
+ return parseDefinition(value);
100
+ }
101
+
102
+ function load(): FleetDefinition {
103
+ const path = configPath();
104
+ if (!existsSync(path))
105
+ throw new FleetUsageError(
106
+ `No fleet configuration. Run devfleet from the project that holds your fleet.json (devfleet fleet-state --example prints one to start from), or name the file with DEVFLEET_CONFIG. A manager whose bucket already holds one restores it with devfleet fleet-state --init. Looked at ${path}.`
107
+ );
108
+ return read(path);
109
+ }
110
+
111
+ /** Where the configuration says the fleet state is kept, read from the file
112
+ * each time: null when it names no bucket, or when this machine has no
113
+ * configuration yet.
114
+ */
115
+ export function declaredState(
116
+ env = process.env,
117
+ home = homedir()
118
+ ): BucketLocation | null {
119
+ const path = configPath(env, home);
120
+ return existsSync(path) ? (read(path).state ?? null) : null;
121
+ }
122
+
123
+ let loaded: FleetDefinition | undefined;
124
+ const current = (): FleetDefinition => (loaded ??= load());
125
+
126
+ /** Forgets the configuration read so far; the next use reads it again. For
127
+ * a command that has just replaced the fleet state under itself.
128
+ */
129
+ export function reloadFleet(): void {
130
+ loaded = undefined;
131
+ }
132
+
133
+ /** This fleet's own configuration: its machines, endpoints, accounts and source
134
+ * repository. Fleet code never names any of them; it reads them here, and this
135
+ * is the only module that loads the file. It is read on first use, so a
136
+ * command that needs no configuration runs on a machine that has none yet.
137
+ */
138
+ export const fleet: FleetDefinition = new Proxy({} as FleetDefinition, {
139
+ get: (_, key) => Reflect.get(current(), key),
140
+ has: (_, key) => Reflect.has(current(), key),
141
+ ownKeys: () => Reflect.ownKeys(current()),
142
+ getOwnPropertyDescriptor: (_, key) =>
143
+ Reflect.getOwnPropertyDescriptor(current(), key),
144
+ });
145
+
146
+ /** Reads one of this fleet's secrets for this node. Tools run from the pinned
147
+ * toolchain, whatever PATH the caller (T3, launchd, systemd) started with.
148
+ */
149
+ export const secret: SecretsProvider = (name) =>
150
+ secretsProvider(fleet.secrets, { run: pinned })(name);
151
+
152
+ /** The same, through an injected runner (fleet-sync phases and their tests). */
153
+ export const secretVia = (run: Runner): SecretsProvider =>
154
+ secretsProvider(fleet.secrets, {
155
+ run: async (argv) => {
156
+ const result = await run(argv, { timeoutMs: 20_000 });
157
+ return result.ok ? result.stdout : null;
158
+ },
159
+ });
160
+
161
+ const pinned: SecretCommand = (argv) =>
162
+ new Promise((resolve) => {
163
+ const child = spawn(argv[0]!, argv.slice(1), {
164
+ stdio: ['ignore', 'pipe', 'ignore'],
165
+ timeout: 20_000,
166
+ env: { ...process.env, PATH: pinnedPath() },
167
+ });
168
+ let output = '';
169
+ child.stdout.on('data', (chunk: Buffer) => {
170
+ if (output.length < 65_536) output += chunk.toString('utf8');
171
+ });
172
+ child.on('error', () => resolve(null));
173
+ child.on('close', (code) => resolve(code === 0 ? output : null));
174
+ });