@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
@@ -0,0 +1,212 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { readFile, unlink, stat } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { prepareSource } from './machine/source';
5
+ import { isRevision } from './machine/state';
6
+ import { sshArgs, shellQuote, sshFailure } from './ssh';
7
+ import type { FleetNode, NodeReport, Runner, Check } from './types';
8
+ import { record } from './config';
9
+ import type { FleetDefinition } from './definition';
10
+ import { cleanCheck, validCheck } from './protocol';
11
+
12
+ export interface BootstrapPayload {
13
+ revision: string;
14
+ script: string;
15
+ bundle: string;
16
+ digest: string;
17
+ authorizeSudo?: string;
18
+ /** The fleet configuration every node keeps at ~/.config/dev-fleet/fleet.json. */
19
+ config?: string;
20
+ }
21
+ /** No manager tool installation; Git/source preparation is part of apply approval. */
22
+ export async function prepareBootstrap(
23
+ root: string,
24
+ revision: string,
25
+ run: Runner,
26
+ options: { definition?: FleetDefinition; checkout?: string } = {}
27
+ ): Promise<BootstrapPayload | null> {
28
+ if (!isRevision(revision)) return null;
29
+ const source = await prepareSource(root, revision, run, options.checkout);
30
+ if (!source) return null;
31
+ const script = await trackedScript(
32
+ source,
33
+ revision,
34
+ 'bootstrap/fleet-remote-bootstrap.sh',
35
+ run
36
+ );
37
+ const sudoScript = await trackedScript(
38
+ source,
39
+ revision,
40
+ 'ansible/ubuntu/authorize-sudo.sh',
41
+ run
42
+ );
43
+ if (!script || !sudoScript) return null;
44
+ const payload = await bundleRevision(
45
+ { root, source, revision },
46
+ { script, authorizeSudo: sudoScript },
47
+ run
48
+ );
49
+ // The fleet configuration is data, not source: it travels beside the code.
50
+ return payload && options.definition
51
+ ? { ...payload, config: JSON.stringify(options.definition, null, 2) + '\n' }
52
+ : payload;
53
+ }
54
+
55
+ /** Installs the approved fleet configuration where every launcher and job on
56
+ * the node reads it. It holds no secret.
57
+ */
58
+ const INSTALL_CONFIG =
59
+ 'umask 077; d="$HOME/.config/dev-fleet"; mkdir -p "$d" && cat >"$d/fleet.json.new" && mv "$d/fleet.json.new" "$d/fleet.json"';
60
+
61
+ /** Workers run one revision and never need history: the bundle holds only the
62
+ * approved commit and its files (about a quarter of the full history), and the
63
+ * receiving checkout is marked shallow at that commit. `git bundle create`
64
+ * cannot cut history, so the v2 bundle is written directly: a header naming
65
+ * the commit, then a pack of exactly its objects.
66
+ */
67
+ const SNAPSHOT_BUNDLE = [
68
+ 'objects=$(git rev-list --objects --no-walk "$1") || exit 1',
69
+ '{ printf \'# v2 git bundle\\n%s refs/personal-fleet/%s\\n\\n\' "$1" "$1"',
70
+ 'printf \'%s\\n\' "$objects" | git pack-objects --stdout -q; } >"$2"',
71
+ ].join('\n');
72
+
73
+ async function bundleRevision(
74
+ {
75
+ root,
76
+ source,
77
+ revision,
78
+ }: { root: string; source: string; revision: string },
79
+ scripts: { script: string; authorizeSudo: string },
80
+ run: Runner
81
+ ): Promise<BootstrapPayload | null> {
82
+ const path = join(root, `transfer-${randomUUID()}.bundle`);
83
+ try {
84
+ const bundled = await run(
85
+ ['sh', '-c', SNAPSHOT_BUNDLE, 'bundle', revision, path],
86
+ { cwd: source, timeoutMs: 120_000 }
87
+ );
88
+ if (!bundled.ok || (await stat(path)).size > 512 * 1024 * 1024) return null;
89
+ const bundle = await readFile(path);
90
+ return {
91
+ revision,
92
+ ...scripts,
93
+ bundle: bundle.toString('base64'),
94
+ digest: createHash('sha256').update(bundle).digest('hex'),
95
+ };
96
+ } finally {
97
+ await unlink(path).catch(() => undefined);
98
+ }
99
+ }
100
+ export async function bootstrapRemote(
101
+ node: FleetNode,
102
+ payload: BootstrapPayload,
103
+ run: Runner
104
+ ): Promise<NodeReport> {
105
+ const cached = await cachedRevision(node, payload.revision, run);
106
+ if (typeof cached !== 'boolean') return { node: node.id, checks: [cached] };
107
+ if (payload.config) {
108
+ const installed = await run([...sshArgs(node), INSTALL_CONFIG], {
109
+ input: payload.config,
110
+ timeoutMs: 20_000,
111
+ });
112
+ if (!installed.ok)
113
+ return { node: node.id, checks: [sshFailure(installed)] };
114
+ }
115
+ const transfer = cached
116
+ ? { mode: 'cached', input: '' }
117
+ : { mode: 'transfer', input: payload.bundle };
118
+ const command = [
119
+ 'sh',
120
+ '-c',
121
+ payload.script,
122
+ 'fleet-bootstrap',
123
+ payload.revision,
124
+ payload.digest,
125
+ node.id,
126
+ transfer.mode,
127
+ ]
128
+ .map(shellQuote)
129
+ .join(' ');
130
+ const result = await run([...sshArgs(node), command], {
131
+ input: transfer.input,
132
+ timeoutMs: 5_400_000,
133
+ });
134
+ const evidence = parseEvidence(result);
135
+ if (evidence) return { node: node.id, checks: evidence };
136
+ if (!result.ok) return { node: node.id, checks: [sshFailure(result)] };
137
+ return {
138
+ node: node.id,
139
+ checks: [
140
+ {
141
+ id: 'bootstrap:protocol',
142
+ status: 'fail',
143
+ summary: 'REMOTE_BOOTSTRAP_EVIDENCE_INVALID',
144
+ remedy: {
145
+ action:
146
+ 'Inspect the source-owned remote operation and lock. Do not assume a manager disconnect stopped installation.',
147
+ },
148
+ },
149
+ ],
150
+ };
151
+ }
152
+
153
+ async function cachedRevision(
154
+ node: FleetNode,
155
+ revision: string,
156
+ run: Runner
157
+ ): Promise<boolean | Check> {
158
+ const command = `d="$HOME/.local/share/personal/fleet/revisions/${revision}"; test ! -L "$d" && test -d "$d/.git" && test "$(git -C "$d" rev-parse HEAD 2>/dev/null)" = '${revision}' && test "$(git -C "$d" rev-parse --show-toplevel 2>/dev/null)" = "$d" && clean=$(git -C "$d" status --porcelain 2>/dev/null) && test -z "$clean" && printf ready`;
159
+ const result = await run([...sshArgs(node), command], { timeoutMs: 20_000 });
160
+ if (!result.ok) {
161
+ const failure = sshFailure(result);
162
+ if (result.reason !== 'exit' || failure.id !== 'ssh:probe') return failure;
163
+ }
164
+ return result.ok && result.stdout === 'ready';
165
+ }
166
+
167
+ function parseEvidence(result: {
168
+ stdout: string;
169
+ ok: boolean;
170
+ }): Check[] | null {
171
+ try {
172
+ const value: unknown = JSON.parse(result.stdout);
173
+ if (
174
+ record(value) &&
175
+ value['version'] === 1 &&
176
+ Array.isArray(value['checks']) &&
177
+ value['checks'].length > 0 &&
178
+ value['checks'].length <= 200 &&
179
+ value['checks'].every(validCheck)
180
+ ) {
181
+ const checks = value['checks'].map(cleanCheck);
182
+ if (!result.ok && !checks.some((check) => check.status === 'fail'))
183
+ checks.push({
184
+ id: 'bootstrap:exit',
185
+ status: 'fail',
186
+ summary: 'REMOTE_BOOTSTRAP_EXIT_FAILED',
187
+ remedy: {
188
+ action:
189
+ 'The remote process failed despite partial evidence. Inspect its operation state before retry; no success receipt may advance.',
190
+ },
191
+ });
192
+ return checks;
193
+ }
194
+ } catch {
195
+ /* Raw SSH output is never included in reports. */
196
+ }
197
+ return null;
198
+ }
199
+
200
+ export async function trackedScript(
201
+ source: string,
202
+ revision: string,
203
+ path: string,
204
+ run: Runner
205
+ ): Promise<string | null> {
206
+ const result = await run(['git', 'show', revision + ':' + path], {
207
+ cwd: source,
208
+ });
209
+ return result.ok && result.stdout.startsWith('#!/bin/sh\n')
210
+ ? result.stdout
211
+ : null;
212
+ }
@@ -0,0 +1,89 @@
1
+ import { configureWorkspace, validConfiguration } from './configure';
2
+ import { parseConfig, record } from './config';
3
+ import { probe } from './probe';
4
+ import { runCommand } from './runner';
5
+ import { syncWorkspace } from './sync';
6
+ import { repairWorkspace, validRepairs } from './repair-runner';
7
+ import { validSnapshot } from './protocol';
8
+ import type { Fingerprint } from './types';
9
+
10
+ async function main(): Promise<void> {
11
+ const raw: unknown = JSON.parse(
12
+ Buffer.from(process.argv.at(-1) ?? '', 'base64').toString()
13
+ );
14
+ if (!validRequest(raw)) throw new Error('Invalid request.');
15
+ const node = parseConfig({ version: 1, nodes: [raw['node']] }).nodes[0]!;
16
+ if (raw['action'] === 'configure') {
17
+ await configureRequest(raw, node);
18
+ } else if (raw['action'] === 'repair') {
19
+ if (
20
+ raw['apply'] !== true ||
21
+ !validRepairs(raw['repairs']) ||
22
+ !validSnapshot({
23
+ version: 1,
24
+ platform: 'request',
25
+ checks: [],
26
+ fingerprint: raw['expected'],
27
+ })
28
+ )
29
+ throw new Error(
30
+ 'Repair requires explicit authorization and supported actions.'
31
+ );
32
+ const checks = await repairWorkspace(
33
+ node.workspace,
34
+ raw['repairs'],
35
+ runCommand,
36
+ raw['expected'] as Fingerprint
37
+ );
38
+ process.stdout.write(JSON.stringify({ version: 1, checks }));
39
+ } else if (raw['action'] === 'sync') {
40
+ if (raw['apply'] !== true)
41
+ throw new Error('Sync requires explicit authorization.');
42
+ const checks = await syncWorkspace(
43
+ node.workspace,
44
+ raw['pull'] === true,
45
+ runCommand
46
+ );
47
+ process.stdout.write(JSON.stringify({ version: 1, checks }));
48
+ } else
49
+ process.stdout.write(
50
+ JSON.stringify(
51
+ await probe(node.workspace, node.required, runCommand, node.id)
52
+ )
53
+ );
54
+ }
55
+
56
+ await main().catch(() => {
57
+ process.stdout.write(
58
+ JSON.stringify({
59
+ version: 1,
60
+ error: 'Remote operation failed; inspect node locally.',
61
+ })
62
+ );
63
+ process.exitCode = 1;
64
+ });
65
+
66
+ async function configureRequest(
67
+ raw: Record<string, unknown>,
68
+ node: { id: string; workspace: string }
69
+ ): Promise<void> {
70
+ if (
71
+ raw['apply'] !== true ||
72
+ !validConfiguration(raw['configuration']) ||
73
+ raw['configuration'].node !== node.id
74
+ )
75
+ throw new Error('Invalid configuration request.');
76
+ const checks = await configureWorkspace(
77
+ node.workspace,
78
+ raw['configuration'],
79
+ runCommand
80
+ );
81
+ process.stdout.write(JSON.stringify({ version: 1, checks }));
82
+ }
83
+
84
+ function validRequest(raw: unknown): raw is Record<string, unknown> {
85
+ return (
86
+ record(raw) &&
87
+ ['probe', 'sync', 'repair', 'configure'].includes(String(raw['action']))
88
+ );
89
+ }
@@ -0,0 +1,12 @@
1
+ /** POSIX lock prelude shared by every remote operation. Linux uses flock(1);
2
+ * macOS lacks it, so the same descriptor is locked through Python's fcntl. The
3
+ * lock belongs to the shell's open descriptor and persists until that exits.
4
+ */
5
+ export function remoteLockPrelude(
6
+ root = '"$HOME/.local/share/personal/fleet"'
7
+ ): string {
8
+ return [
9
+ `exec 9>${root}/remote-operation.lock`,
10
+ 'if command -v flock >/dev/null 2>&1; then flock -n 9; else python3 -c "import fcntl; fcntl.flock(9, fcntl.LOCK_EX | fcntl.LOCK_NB)" 9>&9; fi',
11
+ ].join(' && ');
12
+ }
@@ -0,0 +1,206 @@
1
+ export type AutomatedRepair = 'workspace' | 'workstation' | 'providers';
2
+ export interface RepairRecipe {
3
+ id: string;
4
+ title: string;
5
+ owner: string;
6
+ automatic: boolean;
7
+ action: string;
8
+ commands: string[][];
9
+ }
10
+ const login = (
11
+ id: string,
12
+ title: string,
13
+ commands: string[][],
14
+ action: string
15
+ ): RepairRecipe => ({
16
+ id: `login:${id}`,
17
+ title,
18
+ owner: `${id} native CLI`,
19
+ automatic: false,
20
+ action,
21
+ commands,
22
+ });
23
+ export const REPAIR_RECIPES: RepairRecipe[] = [
24
+ {
25
+ id: 'workspace',
26
+ title: 'Install locked tools and frozen dependencies',
27
+ owner: 'mise / bootstrap/setup.ts',
28
+ automatic: true,
29
+ action:
30
+ 'Runs the existing shared setup and trusted checkout lifecycle scripts. Updates user mise/PATH configuration and writes a sync receipt. Does not pull or reset Git.',
31
+ commands: [
32
+ [
33
+ 'mise',
34
+ 'exec',
35
+ '--locked',
36
+ 'bun',
37
+ '--',
38
+ 'bun',
39
+ 'bootstrap/setup.ts',
40
+ '--non-interactive',
41
+ ],
42
+ ],
43
+ },
44
+ {
45
+ id: 'workstation',
46
+ title: 'Install the devfleet launcher',
47
+ owner: 'dev-fleet',
48
+ automatic: true,
49
+ action:
50
+ 'Links ~/.local/bin/devfleet to this checkout. Nothing is copied and no configuration is changed.',
51
+ commands: [['sh', 'bootstrap/install-launcher.sh']],
52
+ },
53
+ {
54
+ id: 'providers',
55
+ title: "Refresh this machine's own service credentials",
56
+ owner: 'dev-fleet',
57
+ automatic: true,
58
+ action:
59
+ "Renews this machine's own secrets-provider session and the service logins derived from it. No credentials are copied from another computer.",
60
+ commands: [['bun', 'bootstrap/worker-auth.ts']],
61
+ },
62
+ login(
63
+ 'codex',
64
+ 'Sign in to Codex on this machine',
65
+ [['codex', 'login', '--device-auth']],
66
+ 'Approve the device flow in your browser. Run codex login status afterward; retain this machine’s own credential store.'
67
+ ),
68
+ login(
69
+ 'claude',
70
+ 'Sign in to Claude Code on this machine',
71
+ [['claude', 'auth', 'login']],
72
+ 'Complete the native browser login, then run claude auth status --json. API-key and subscription contexts can differ.'
73
+ ),
74
+ login(
75
+ 'github',
76
+ 'Sign in to GitHub and configure Git',
77
+ [
78
+ [
79
+ 'gh',
80
+ 'auth',
81
+ 'login',
82
+ '--hostname',
83
+ 'github.com',
84
+ '--web',
85
+ '--git-protocol',
86
+ 'https',
87
+ ],
88
+ ['gh', 'auth', 'setup-git'],
89
+ ],
90
+ 'Approve GitHub in the browser on the intended account, then run gh auth status --active. Environment tokens can override stored login; inspect their source locally.'
91
+ ),
92
+ login(
93
+ 'vault',
94
+ 'Authorize this machine for the shared Vault',
95
+ [],
96
+ 'Run vault login -method=userpass username=YOUR_USERNAME in your own terminal. The native CLI prompts privately for the password. Use the existing permitted project/config; never copy VAULT_TOKEN between computers.'
97
+ ),
98
+ login(
99
+ 'railway',
100
+ 'Sign in to Railway',
101
+ [['railway', 'login', '--browserless']],
102
+ 'Complete the browserless native flow, then run railway whoami --json.'
103
+ ),
104
+ login(
105
+ 'vercel',
106
+ 'Sign in to Vercel',
107
+ [['vercel', 'login']],
108
+ 'Complete native login, then run vercel whoami. Select the intended team/project separately; do not create resources as a login repair.'
109
+ ),
110
+ login(
111
+ 'cloudflare',
112
+ 'Sign in to Cloudflare',
113
+ [['wrangler', 'login', '--browser=false']],
114
+ 'Complete native OAuth login, then run wrangler whoami --json. For headless SSH, prefer an existing scoped API token injected by the approved Vault config. --browser=false suppresses browser launch; it does not make a remote loopback callback reachable. Any OAuth callback tunnel needs explicit operator setup. Resource access still depends on account permissions.'
115
+ ),
116
+ login(
117
+ 'aws',
118
+ 'Refresh the selected AWS SSO session',
119
+ [['aws', 'sso', 'login']],
120
+ 'Configure the intended profile with aws configure sso first if needed. Login and the health check use this shell’s current profile; no access keys are cloned.'
121
+ ),
122
+ login(
123
+ 'npm',
124
+ 'Sign in to npm on this machine',
125
+ [
126
+ [
127
+ 'npm',
128
+ 'login',
129
+ '--auth-type=web',
130
+ '--registry=https://registry.npmjs.org/',
131
+ ],
132
+ ],
133
+ 'Complete the specific CLI approval and run npm whoami. Prefer the existing trusted-publisher CI workflow for releases.'
134
+ ),
135
+ login(
136
+ 'opencode',
137
+ 'Authorize an OpenCode provider',
138
+ [['opencode', 'auth', 'login']],
139
+ 'Prefer the existing Vault-backed providers repair for managed API keys. Native auth login handles an operator-owned OAuth provider. Credential listing alone does not prove live provider access.'
140
+ ),
141
+ login(
142
+ 'copilot',
143
+ 'Sign in to Copilot',
144
+ [['copilot', 'login']],
145
+ 'Complete native login; provider authorization still needs supported operator diagnostics. No model prompt is used for health checking.'
146
+ ),
147
+ login(
148
+ 'gemini',
149
+ 'Sign in to Gemini',
150
+ [],
151
+ 'Open Gemini interactively and choose its supported sign-in flow. Provider authorization remains unverified without a supported read-only status adapter.'
152
+ ),
153
+ {
154
+ id: 't3',
155
+ title: 'Enable and authorize T3 Connect',
156
+ owner: 'T3',
157
+ automatic: false,
158
+ action:
159
+ 'Run t3 up --connect in this machine’s terminal and approve the intended connection. Treat pairing material as private. Then check t3 status --json and perform an operator peer test.',
160
+ commands: [['t3', 'up', '--connect']],
161
+ },
162
+ {
163
+ id: 'docker',
164
+ title: 'Start the intended Docker runtime',
165
+ owner: 'OS / Docker',
166
+ automatic: false,
167
+ action:
168
+ 'Start Docker Desktop on desktop systems, or the already-installed Docker service using the OS service manager on Linux. Confirm the intended context with docker context ls. This repair never installs a daemon, invokes sudo, changes contexts or exposes a TCP socket.',
169
+ commands: [
170
+ ['docker', 'context', 'ls'],
171
+ ['docker', 'info'],
172
+ ],
173
+ },
174
+ {
175
+ id: 'password-manager',
176
+ title: 'Review shared human login onboarding',
177
+ owner: 'password-manager / Bitwarden',
178
+ automatic: false,
179
+ action:
180
+ 'Use the existing password-manager doctor and actions list on this computer. Its setup guides native consent and Bitwarden enrollment. Keep its private state and OS credential store local. Password vault access does not authorize native CLI OAuth sessions; imports and rotations remain separate reviewed workflows.',
181
+ commands: [
182
+ ['bun', 'run', 'password-manager', 'doctor'],
183
+ ['bun', 'run', 'password-manager', 'actions', 'list'],
184
+ ],
185
+ },
186
+ {
187
+ id: 'operator',
188
+ title: 'Resolve the check with its owning tool',
189
+ owner: 'operator / existing owner',
190
+ automatic: false,
191
+ action:
192
+ 'Follow the check’s remedy on the affected machine. Verify host fingerprints, register real SSH aliases/checkouts, reconcile Git changes, or perform the named end-to-end test. Infrastructure changes remain OpenTofu-owned.',
193
+ commands: [],
194
+ },
195
+ ];
196
+ export const AUTOMATED_REPAIRS: AutomatedRepair[] = [
197
+ 'workspace',
198
+ 'workstation',
199
+ 'providers',
200
+ ];
201
+ export function recipe(id: string): RepairRecipe {
202
+ return (
203
+ REPAIR_RECIPES.find((entry) => entry.id === id) ??
204
+ REPAIR_RECIPES.find((entry) => entry.id === 'operator')!
205
+ );
206
+ }
@@ -0,0 +1,78 @@
1
+ import { AUTOMATED_REPAIRS, recipe } from './repair-catalog';
2
+ import type { RepairRecipe } from './repair-catalog';
3
+ import type { Check, FleetNode, NodeReport, Fingerprint } from './types';
4
+
5
+ export interface RepairTarget {
6
+ name: string;
7
+ root: string;
8
+ node?: FleetNode;
9
+ fingerprint?: Fingerprint;
10
+ }
11
+ export interface RepairStep extends RepairRecipe {
12
+ node: string;
13
+ checks: string[];
14
+ eligible: boolean;
15
+ next: string[];
16
+ }
17
+
18
+ export function repairId(check: Check): string {
19
+ if (check.id.startsWith('tool:docker-')) return 'docker';
20
+ if (check.id.startsWith('tool:github-auth')) return 'login:github';
21
+ if (['dependencies', 'auth:toolchain'].includes(check.id)) return 'workspace';
22
+ if (check.id.startsWith('tool:')) return 'workspace';
23
+ if (check.id.startsWith('workstation:')) return 'workstation';
24
+ if (
25
+ check.id.startsWith('auth:') &&
26
+ !['auth:vscode', 'auth:workstation'].includes(check.id)
27
+ )
28
+ return `login:${check.id.slice(5)}`;
29
+ if (check.id === 't3:settings' || check.id === 't3:api') return 't3';
30
+ return 'operator';
31
+ }
32
+
33
+ /** Every nonpassing check is accounted for; unknown checks remain operator work. */
34
+ export function repairPlan(
35
+ reports: NodeReport[],
36
+ targets: RepairTarget[]
37
+ ): RepairStep[] {
38
+ const steps: RepairStep[] = [];
39
+ for (const report of reports) {
40
+ const groups = new Map<string, Check[]>();
41
+ for (const check of report.checks.filter(
42
+ (entry) => entry.status !== 'pass'
43
+ )) {
44
+ const repair = recipe(repairId(check)).id;
45
+ groups.set(repair, [...(groups.get(repair) ?? []), check]);
46
+ }
47
+ for (const [id, checks] of groups)
48
+ steps.push({
49
+ ...recipe(id),
50
+ node: report.node,
51
+ eligible:
52
+ targets.some((target) => target.name === report.node) &&
53
+ report.snapshot !== undefined,
54
+ checks: checks.map((check) => check.id),
55
+ next: [
56
+ ...new Set(
57
+ checks.flatMap((check) =>
58
+ check.remedy ? [check.remedy.action] : [check.summary]
59
+ )
60
+ ),
61
+ ],
62
+ });
63
+ }
64
+ return steps;
65
+ }
66
+
67
+ /** Prerequisite owners always run first, independent of check or selector order. */
68
+ export function automaticRepairs(
69
+ steps: RepairStep[],
70
+ node: string
71
+ ): RepairStep[] {
72
+ return AUTOMATED_REPAIRS.flatMap((id) =>
73
+ steps.filter(
74
+ (step) =>
75
+ step.node === node && step.id === id && step.automatic && step.eligible
76
+ )
77
+ );
78
+ }
@@ -0,0 +1,84 @@
1
+ import { fingerprint } from './fingerprint';
2
+ import { AUTOMATED_REPAIRS, recipe } from './repair-catalog';
3
+ import { syncWorkspace } from './sync';
4
+ import type { AutomatedRepair } from './repair-catalog';
5
+ import type { Check, Runner, Fingerprint } from './types';
6
+
7
+ export function validRepairs(value: unknown): value is AutomatedRepair[] {
8
+ return (
9
+ Array.isArray(value) &&
10
+ value.length > 0 &&
11
+ value.length <= 3 &&
12
+ new Set(value).size === value.length &&
13
+ value.every((id) => AUTOMATED_REPAIRS.includes(id as AutomatedRepair))
14
+ );
15
+ }
16
+
17
+ /** Commands come exclusively from the allowlist, never from probe output or a roster. */
18
+ export async function repairWorkspace(
19
+ root: string,
20
+ repairs: AutomatedRepair[],
21
+ run: Runner,
22
+ expected?: Fingerprint
23
+ ): Promise<Check[]> {
24
+ if (!validRepairs(repairs)) throw new Error('Unsupported repairs');
25
+ const before = await fingerprint(root, run);
26
+ if (!before.head || !Object.values(before.files).every(Boolean))
27
+ return [
28
+ {
29
+ id: 'repair:workspace',
30
+ status: 'fail',
31
+ summary:
32
+ 'Repairs require a complete existing checkout. Register/bootstrap it first.',
33
+ },
34
+ ];
35
+ if (expected && JSON.stringify(before) !== JSON.stringify(expected))
36
+ return [
37
+ {
38
+ id: 'repair:inputs',
39
+ status: 'fail',
40
+ summary:
41
+ 'Checkout inputs changed since diagnostics. Generate a fresh repair plan before applying.',
42
+ },
43
+ ];
44
+ const checks: Check[] = [];
45
+ for (const id of AUTOMATED_REPAIRS.filter((entry) =>
46
+ repairs.includes(entry)
47
+ )) {
48
+ const result =
49
+ id === 'workspace'
50
+ ? await syncWorkspace(root, false, run)
51
+ : await ownerCommand(root, id, run);
52
+ checks.push(...result);
53
+ if (result.some((check) => check.status !== 'pass')) break;
54
+ }
55
+ const after = await fingerprint(root, run);
56
+ if (JSON.stringify(before) !== JSON.stringify(after))
57
+ checks.push({
58
+ id: 'repair:inputs',
59
+ status: 'fail',
60
+ summary:
61
+ 'Checkout inputs changed during repair. Stop and review the checkout before retrying.',
62
+ });
63
+ return checks;
64
+ }
65
+
66
+ async function ownerCommand(
67
+ root: string,
68
+ id: AutomatedRepair,
69
+ run: Runner
70
+ ): Promise<Check[]> {
71
+ const result = await run(recipe(id).commands[0]!, {
72
+ cwd: root,
73
+ timeoutMs: 1_200_000,
74
+ });
75
+ return [
76
+ {
77
+ id: `repair:${id}`,
78
+ status: result.ok ? 'pass' : 'fail',
79
+ summary: result.ok
80
+ ? 'The existing owner command completed. Fresh diagnostics determine whether configuration and authorization now pass.'
81
+ : `The owner command failed (${result.reason}); partial changes are possible. Raw output and credentials are withheld. Later repairs on this computer were stopped.`,
82
+ },
83
+ ];
84
+ }