@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,185 @@
1
+ #!/bin/sh
2
+ # Executed over strict SSH from the selected verified Git revision. stdin is a
3
+ # base64 Git bundle. All executable content remains in this tracked source.
4
+ set -eu
5
+ umask 077
6
+ revision=$1
7
+ bundle_sha=$2
8
+ node=$3
9
+ source_mode=${4:-transfer}
10
+ case "$source_mode" in cached|transfer) ;; *) exit 2;; esac
11
+ case "$revision" in *[!a-f0-9]*|'') exit 2;; esac
12
+ [ "${#revision}" = 40 ] || exit 2
13
+ case "$bundle_sha" in *[!a-f0-9]*|'') exit 2;; esac
14
+ [ "${#bundle_sha}" = 64 ] || exit 2
15
+ case "$node" in *[!a-z0-9-]*|'') exit 2;; esac
16
+ phase=bootstrap
17
+ bundle=''
18
+ code=0
19
+ failure_code=REMOTE_BOOTSTRAP_INCOMPLETE
20
+ die() { failure_code=$1; exit 1; }
21
+ fail() {
22
+ printf '{"version":1,"checks":[{"id":"bootstrap:%s","status":"fail","summary":"%s","remedy":{"action":"Inspect prerequisites and the remote operation lock, then retry the approved revision. No success receipt advanced."}}]}\n' "$phase" "$failure_code"
23
+ }
24
+ trap 'code=$?; [ -z "$bundle" ] || rm -f -- "$bundle"; if [ "$code" != 0 ]; then fail; fi' EXIT
25
+ # Preserve the lock holder if its SSH channel disappears mid-install.
26
+ trap '' HUP
27
+ trap 'exit 1' INT TERM
28
+ # No shell startup assumptions, and no implicit root development user.
29
+ [ "$(id -u)" != 0 ] || die ROOT_DEVELOPMENT_USER_REJECTED
30
+ # Ubuntu 26.04 is the accepted worker platform; macOS 14+ follows the same
31
+ # phases best effort with launchd instead of systemd.
32
+ platform=$(uname -s)
33
+ case "$platform" in
34
+ Linux)
35
+ # shellcheck source=/dev/null
36
+ . /etc/os-release
37
+ [ "$ID" = ubuntu ] && [ "$VERSION_ID" = 26.04 ] || die UBUNTU_BASELINE_MISMATCH
38
+ case "$(uname -m)" in x86_64|aarch64) ;; *) die UBUNTU_ARCHITECTURE_UNSUPPORTED;; esac ;;
39
+ Darwin)
40
+ major=$(sw_vers -productVersion | cut -d. -f1)
41
+ case "$major" in ''|*[!0-9]*) die MACOS_BASELINE_MISMATCH;; esac
42
+ [ "$major" -ge 14 ] || die MACOS_BASELINE_MISMATCH
43
+ case "$(uname -m)" in arm64|x86_64) ;; *) die MACOS_ARCHITECTURE_UNSUPPORTED;; esac ;;
44
+ *) die PLATFORM_UNSUPPORTED;;
45
+ esac
46
+ root="$HOME/.local/share/personal/fleet"
47
+ # Refuse symlinked state roots rather than allowing writes through them.
48
+ for path in "$HOME/.local" "$HOME/.local/share" "$HOME/.local/share/personal" "$root"; do
49
+ [ ! -L "$path" ] || die FLEET_STATE_PATH_UNSAFE
50
+ done
51
+ mkdir -p "$root"
52
+ phase=lock
53
+ [ ! -L "$root/remote-operation.lock" ] || exit 1
54
+ exec 9>"$root/remote-operation.lock"
55
+ # macOS has no flock(1); lock the same descriptor through fcntl. The lock
56
+ # follows this shell's open descriptor and is released when it exits.
57
+ if command -v flock >/dev/null 2>&1; then
58
+ flock -n 9 || die FLEET_REMOTE_LOCKED
59
+ else
60
+ command -v python3 >/dev/null 2>&1 || die BOOTSTRAP_LOCK_TOOL_REQUIRED
61
+ python3 -c 'import fcntl; fcntl.flock(9, fcntl.LOCK_EX | fcntl.LOCK_NB)' 9>&9 || die FLEET_REMOTE_LOCKED
62
+ fi
63
+ printf '{"version":1,"pid":%s,"revision":"%s","node":"%s"}\n' "$$" "$revision" "$node" >"$root/remote-operation.json"
64
+ phase=resources
65
+ # A bootstrap floor protects source transfer and minimal prerequisites. The
66
+ # Workstation baseline subsequently enforces its stricter declared capacity.
67
+ free_kib=$(df -Pk "$root" | awk 'END {print $4}')
68
+ free_inodes=$(df -Pi "$root" | awk 'END {print $4}')
69
+ case "$free_kib:$free_inodes" in *[!0-9:]*|:*|*:) die BOOTSTRAP_CAPACITY_UNVERIFIED;; esac
70
+ [ "$free_kib" -ge 2097152 ] && [ "$free_inodes" -ge 50000 ] || die BOOTSTRAP_DISK_CAPACITY_REQUIRED
71
+ phase=source-transfer
72
+ if [ "$source_mode" = transfer ]; then
73
+ bundle=$(mktemp "$root/source.XXXXXX")
74
+ # Leave no executable payload copies; this temporary file is only Git data.
75
+ base64 -d >"$bundle"
76
+ if command -v sha256sum >/dev/null 2>&1; then
77
+ printf '%s %s\n' "$bundle_sha" "$bundle" | sha256sum -c - >/dev/null 2>&1 || die SOURCE_DIGEST_MISMATCH
78
+ else
79
+ [ "$(shasum -a 256 "$bundle" | cut -d' ' -f1)" = "$bundle_sha" ] || die SOURCE_DIGEST_MISMATCH
80
+ fi
81
+ fi
82
+ phase=prerequisites
83
+ if [ "$platform" = Linux ]; then
84
+ missing=''
85
+ for package in git curl ca-certificates unzip xz-utils build-essential python3 ansible-core; do
86
+ dpkg-query -W -f='${db:Status-Status}' "$package" 2>/dev/null | grep -qx installed || missing="$missing $package"
87
+ done
88
+ if [ -n "$missing" ]; then
89
+ sudo -n true >/dev/null 2>&1 || die SUDO_AUTHORIZATION_REQUIRED
90
+ # Never remove APT locks or kill package processes. Timeout keeps genuine
91
+ # transactions bounded; Ubuntu archive signatures/TLS remain enabled.
92
+ sudo -n timeout 300 apt-get -o DPkg::Lock::Timeout=120 update >/dev/null 2>&1 || die APT_UPDATE_FAILED
93
+ # shellcheck disable=SC2086
94
+ sudo -n timeout 600 env DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=120 --no-install-recommends install -y $missing >/dev/null 2>&1 || die APT_INSTALL_FAILED
95
+ fi
96
+ else
97
+ # Apple Command Line Tools provide git and python3; Ansible runs through the
98
+ # pinned uv tool runner installed by the shared mise toolchain below.
99
+ xcode-select -p >/dev/null 2>&1 || die XCODE_COMMAND_LINE_TOOLS_REQUIRED
100
+ command -v git >/dev/null 2>&1 || die XCODE_COMMAND_LINE_TOOLS_REQUIRED
101
+ fi
102
+ phase=source-verification
103
+ deployment="$root/revisions/$revision"
104
+ [ ! -L "$root/revisions" ] && [ ! -L "$deployment" ] || exit 1
105
+ mkdir -p "$deployment"
106
+ if [ ! -e "$deployment/.git" ]; then
107
+ [ -z "$(ls -A "$deployment")" ] || exit 1
108
+ git -C "$deployment" init -q >/dev/null 2>&1 || exit 1
109
+ fi
110
+ # An interrupted initial fetch/checkout can leave an empty initialized repository.
111
+ # Resume only that state; an existing commit or untracked work is never reset.
112
+ if ! git -C "$deployment" rev-parse --verify HEAD >/dev/null 2>&1; then
113
+ [ "$source_mode" = transfer ] || die SOURCE_CACHE_CHANGED
114
+ [ "$(git -C "$deployment" rev-parse --show-toplevel)" = "$deployment" ] || exit 1
115
+ clean=$(git -C "$deployment" status --porcelain) || die SOURCE_STATUS_FAILED
116
+ [ -z "$clean" ] || die SOURCE_CHECKOUT_CONFLICT
117
+ # The bundle holds this commit without its history: declare the checkout
118
+ # shallow at it so Git accepts a commit whose parents are absent.
119
+ printf '%s\n' "$revision" >"$deployment/.git/shallow"
120
+ git -C "$deployment" fetch --no-tags "$bundle" "$revision" >/dev/null 2>&1 || exit 1
121
+ git -C "$deployment" checkout --detach "$revision" >/dev/null 2>&1 || exit 1
122
+ fi
123
+ [ -z "$bundle" ] || rm -f "$bundle"
124
+ bundle=''
125
+ [ "$(git -C "$deployment" rev-parse HEAD)" = "$revision" ] || exit 1
126
+ [ "$(git -C "$deployment" rev-parse --show-toplevel)" = "$deployment" ] || exit 1
127
+ clean=$(git -C "$deployment" status --porcelain) || die SOURCE_STATUS_FAILED
128
+ [ -z "$clean" ] || die SOURCE_CHECKOUT_CONFLICT
129
+ phase=toolchain
130
+ cd "$deployment"
131
+ export PATH="$HOME/.local/bin:$HOME/.local/share/mise/shims:/usr/local/bin:/usr/bin:/bin"
132
+ status_file="$root/toolchain-status.json"
133
+ ready_file="$root/toolchain-ready"
134
+ [ ! -L "$status_file" ] && [ ! -L "$ready_file" ] || die FLEET_STATE_PATH_UNSAFE
135
+ # Warm: the same toolchain inputs (tool pins, locks and the setup code itself)
136
+ # passed within a day, so a revision that changes none of them skips the full
137
+ # setup. Changed inputs, a failure or an older stamp run it; agent versions
138
+ # are still checked (and reinstalled on drift) by node convergence.
139
+ toolchain_key=$(git rev-parse HEAD:mise.toml HEAD:mise.lock HEAD:.mise HEAD:bun.lock HEAD:package.json HEAD:bootstrap 2>/dev/null | tr '\n' ' ')
140
+ [ -n "$toolchain_key" ] || toolchain_key=$revision
141
+ if [ "$(cat "$ready_file" 2>/dev/null)" = "$toolchain_key" ] && [ -n "$(find "$ready_file" -mmin -1440 2>/dev/null)" ]; then
142
+ toolchain_warm=1
143
+ else
144
+ toolchain_warm=''
145
+ rm -f -- "$status_file" "$ready_file"
146
+ fi
147
+ if command -v timeout >/dev/null 2>&1; then bounded='timeout 1800'; else bounded=''; fi
148
+ # shellcheck disable=SC2086
149
+ if [ -z "$toolchain_warm" ] && ! $bounded bash bootstrap/bootstrap.sh --worker --non-interactive --status-file "$status_file" >/dev/null 2>&1; then
150
+ tool_phase=$(sed -n 's/.*"phase":"\([a-z0-9-]*\)".*/\1/p' "$status_file" 2>/dev/null) || tool_phase=''
151
+ case "$tool_phase" in
152
+ managed-config) die TOOLCHAIN_CONFIG_CONFLICT;;
153
+ mise) die TOOLCHAIN_MISE_FAILED;;
154
+ aws) die TOOLCHAIN_AWS_FAILED;;
155
+ t3) die TOOLCHAIN_T3_FAILED;;
156
+ global-config|shell) die TOOLCHAIN_PATH_CONFIGURATION_FAILED;;
157
+ dependencies) die TOOLCHAIN_FROZEN_DEPENDENCIES_FAILED;;
158
+ *) die TOOLCHAIN_CONVERGENCE_FAILED;;
159
+ esac
160
+ fi
161
+ # A new revision's checkout still needs its own (cached, frozen) dependencies.
162
+ if [ -n "$toolchain_warm" ] && [ ! -d node_modules ]; then
163
+ # shellcheck disable=SC2086
164
+ $bounded mise exec --locked bun -- bun install --frozen-lockfile >/dev/null 2>&1 || die TOOLCHAIN_FROZEN_DEPENDENCIES_FAILED
165
+ fi
166
+ [ -n "$toolchain_warm" ] || printf '%s' "$toolchain_key" >"$ready_file"
167
+ # Every synced revision is a full checkout with its own dependencies (about
168
+ # 1 GB). Keep this one, the one T3's providers launch from and the two most
169
+ # recently used others; the rest would slowly fill the disk.
170
+ phase=cleanup
171
+ touch "$deployment"
172
+ applied=$(basename "$(readlink "$root/current" 2>/dev/null || true)")
173
+ kept=0
174
+ # shellcheck disable=SC2012
175
+ for old in $(ls -1t "$root/revisions"); do
176
+ case "$old" in *[!a-f0-9]*) continue;; esac
177
+ [ "${#old}" = 40 ] || continue
178
+ [ "$old" != "$revision" ] && [ "$old" != "$applied" ] || continue
179
+ kept=$((kept + 1))
180
+ [ "$kept" -gt 2 ] || continue
181
+ rm -rf -- "${root:?}/revisions/$old" || true
182
+ done
183
+ phase=configuration
184
+ trap - EXIT INT TERM
185
+ exec mise exec --locked bun -- bun src/node-converge.ts "$node" "$revision"
@@ -0,0 +1,113 @@
1
+ /** Publish one managed global mise config without clobbering unrelated configuration. */
2
+ import {
3
+ cpSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ statSync,
8
+ writeFileSync,
9
+ } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { homedir } from 'node:os';
12
+
13
+ const MARKER = '# Managed by dev-fleet bootstrap/setup.ts\n';
14
+ /**
15
+ * Where agent harnesses create a fresh checkout per task. Every new path would
16
+ * need its own `mise trust`, and an untrusted mise.toml makes every mise shim
17
+ * (claude, codex, bun, ...) exit before the agent starts. mise honours this
18
+ * setting only in the global config, so it is added at publish time and never
19
+ * written into the repository's mise.toml.
20
+ */
21
+ export const AGENT_WORKTREE_ROOTS = ['~/.t3/worktrees'];
22
+ const MANAGED = /^# Managed by \S+ \S*setup\.ts\n/;
23
+ export const workspaceRoot = join(import.meta.dirname, '..');
24
+
25
+ export function globalConfigDir(): string {
26
+ if (process.env['MISE_GLOBAL_CONFIG_FILE']) {
27
+ throw new Error(
28
+ 'Unset MISE_GLOBAL_CONFIG_FILE before applying the shared toolchain.'
29
+ );
30
+ }
31
+ return (
32
+ process.env['MISE_CONFIG_DIR'] ??
33
+ join(process.env['XDG_CONFIG_HOME'] ?? join(homedir(), '.config'), 'mise')
34
+ );
35
+ }
36
+
37
+ export function assertManagedConfig(dir: string): void {
38
+ const config = join(dir, 'config.toml');
39
+ const lock = join(dir, 'mise.lock');
40
+ // Any earlier owner name counts: the marker text identifies the file as
41
+ // managed, never the repository or path it was published from.
42
+ const managed =
43
+ existsSync(config) && MANAGED.test(readFileSync(config, 'utf8'));
44
+ if ((existsSync(config) && !managed) || (existsSync(lock) && !managed)) {
45
+ throw new Error(
46
+ `Existing mise configuration at ${dir}. Back up and move config.toml and mise.lock, then rerun. Nothing was overwritten.`
47
+ );
48
+ }
49
+ }
50
+
51
+ export function publishGlobalConfig(dir: string, root = workspaceRoot): void {
52
+ assertManagedConfig(dir);
53
+ mkdirSync(dir, { recursive: true });
54
+ const path = join(dir, 'config.toml');
55
+ const previous = existsSync(path) ? readFileSync(path, 'utf8') : undefined;
56
+ const config =
57
+ MARKER +
58
+ withTrustedPaths(
59
+ readFileSync(join(root, 'mise.toml'), 'utf8'),
60
+ trustedPaths(previous)
61
+ );
62
+ if (previous !== config) writeFileSync(path, config);
63
+ cpSync(join(root, 'mise.lock'), join(dir, 'mise.lock'), {
64
+ filter: changedFile,
65
+ });
66
+ const sidecars = join(root, '.mise/locks');
67
+ if (existsSync(sidecars))
68
+ cpSync(sidecars, join(dir, '.mise/locks'), {
69
+ recursive: true,
70
+ filter: changedFile,
71
+ });
72
+ }
73
+
74
+ /**
75
+ * The managed roots plus any entries already in the published config, so a
76
+ * local `mise settings add trusted_config_paths ...` survives the next publish.
77
+ */
78
+ export function trustedPaths(previous: string | undefined): string[] {
79
+ let local: unknown;
80
+ try {
81
+ local = previous
82
+ ? (Bun.TOML.parse(previous) as { settings?: Record<string, unknown> })
83
+ .settings?.['trusted_config_paths']
84
+ : undefined;
85
+ } catch {
86
+ local = undefined;
87
+ }
88
+ const extra = Array.isArray(local)
89
+ ? local.filter((path): path is string => typeof path === 'string')
90
+ : [];
91
+ return [...new Set([...AGENT_WORKTREE_ROOTS, ...extra])];
92
+ }
93
+
94
+ /** Adds trusted_config_paths to the [settings] table, creating it if absent. */
95
+ export function withTrustedPaths(config: string, paths: string[]): string {
96
+ const line = `trusted_config_paths = [${paths.map((path) => JSON.stringify(path)).join(', ')}]`;
97
+ if (/^trusted_config_paths\s*=/m.test(config))
98
+ throw new Error(
99
+ 'mise.toml sets trusted_config_paths; mise ignores it outside the global config. Add roots to AGENT_WORKTREE_ROOTS instead.'
100
+ );
101
+ const header = /^\[settings\][^\S\n]*\n/m;
102
+ return header.test(config)
103
+ ? config.replace(header, (match) => `${match}${line}\n`)
104
+ : `${config.replace(/\n*$/, '\n')}\n[settings]\n${line}\n`;
105
+ }
106
+
107
+ function changedFile(source: string, destination: string): boolean {
108
+ return (
109
+ statSync(source).isDirectory() ||
110
+ !existsSync(destination) ||
111
+ !readFileSync(source).equals(readFileSync(destination))
112
+ );
113
+ }
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+ # Links ~/.local/bin/devfleet to this checkout (or the one given). Idempotent.
3
+ set -eu
4
+ root=${1:-$(cd "$(dirname "$0")/.." && pwd)}
5
+ [ -x "$root/bootstrap/devfleet" ] || exit 1
6
+ mkdir -p "$HOME/.local/bin"
7
+ ln -sfn "$root/bootstrap/devfleet" "$HOME/.local/bin/devfleet"
@@ -0,0 +1,86 @@
1
+ /** AWS v2's official Windows MSI; aqua only packages AWS for Unix. */
2
+ import { createHash } from 'node:crypto';
3
+ import { appendFileSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { spawnSync } from 'node:child_process';
7
+ import { workspaceRoot } from './global-config';
8
+
9
+ type AwsConfig = {
10
+ tools: { aws: { version: string } };
11
+ vars: { aws_windows_msi_sha256: string };
12
+ };
13
+ export function awsWindowsArtifact(config: AwsConfig): {
14
+ url: string;
15
+ sha256: string;
16
+ version: string;
17
+ } {
18
+ const version = config.tools.aws.version;
19
+ const sha256 = config.vars.aws_windows_msi_sha256;
20
+ if (!/^2\.\d+\.\d+$/.test(version) || !/^[a-f0-9]{64}$/.test(sha256))
21
+ throw new Error('Invalid pinned AWS Windows artifact.');
22
+ return {
23
+ version,
24
+ url: `https://awscli.amazonaws.com/AWSCLIV2-${version}.msi`,
25
+ sha256,
26
+ };
27
+ }
28
+
29
+ export async function installNativeAws(): Promise<void> {
30
+ if (process.platform !== 'win32') return;
31
+ const config = Bun.TOML.parse(
32
+ readFileSync(join(workspaceRoot, 'mise.toml'), 'utf8')
33
+ ) as AwsConfig;
34
+ const artifact = awsWindowsArtifact(config);
35
+ const bin = join(
36
+ process.env['ProgramFiles'] ?? 'C:\\Program Files',
37
+ 'Amazon',
38
+ 'AWSCLIV2'
39
+ );
40
+ const executable = join(bin, 'aws.exe');
41
+ const probe = () =>
42
+ spawnSync(executable, ['--version'], { encoding: 'utf8', timeout: 30_000 });
43
+ if (!probe().stdout?.includes(`aws-cli/${artifact.version} `)) {
44
+ const directory = mkdtempSync(join(tmpdir(), 'workspace-aws-'));
45
+ try {
46
+ const response = await fetch(artifact.url);
47
+ if (!response.ok)
48
+ throw new Error(`AWS MSI download failed (${response.status}).`);
49
+ const bytes = Buffer.from(await response.arrayBuffer());
50
+ if (createHash('sha256').update(bytes).digest('hex') !== artifact.sha256)
51
+ throw new Error('AWS MSI checksum mismatch.');
52
+ const msi = join(directory, 'aws.msi');
53
+ await Bun.write(msi, bytes);
54
+ const script = String.raw`$arguments = @('/i', ('"' + $env:WORKSPACE_AWS_MSI + '"'), '/qn', '/norestart'); $options = @{FilePath='msiexec.exe'; ArgumentList=$arguments; Wait=$true; PassThru=$true}; if ($env:WORKSPACE_AWS_ELEVATE -eq '1') { $options.Verb = 'RunAs' }; $p = Start-Process @options; exit $p.ExitCode`;
55
+ const result = spawnSync(
56
+ 'powershell.exe',
57
+ ['-NoProfile', '-Command', script],
58
+ {
59
+ stdio: 'inherit',
60
+ env: {
61
+ ...process.env,
62
+ WORKSPACE_AWS_MSI: msi,
63
+ WORKSPACE_AWS_ELEVATE:
64
+ process.argv.includes('--non-interactive') || process.env['CI']
65
+ ? '0'
66
+ : '1',
67
+ },
68
+ }
69
+ );
70
+ if (result.status !== 0 && result.status !== 3010)
71
+ throw new Error(
72
+ `AWS MSI installation failed (exit ${result.status}, signal ${result.signal}, error ${result.error?.message ?? 'none'}); check for a conflicting installed version or run the reviewed bootstrap as Administrator.`
73
+ );
74
+ } finally {
75
+ rmSync(directory, { recursive: true, force: true });
76
+ }
77
+ }
78
+ if (!probe().stdout?.includes(`aws-cli/${artifact.version} `))
79
+ throw new Error(
80
+ `Expected AWS CLI ${artifact.version}; resolve the existing installation before retrying.`
81
+ );
82
+ process.env['PATH'] = `${bin};${process.env['PATH'] ?? ''}`;
83
+ // Persist across subsequent hosted-runner steps; normal users reopen the shell.
84
+ if (process.env['GITHUB_PATH'])
85
+ appendFileSync(process.env['GITHUB_PATH'], `${bin}\n`);
86
+ }
@@ -0,0 +1,154 @@
1
+ /** Pinned Ubuntu T3 distribution. Shared tool installation owns artifacts;
2
+ * Workstation owns services. Never touches ~/.t3 or replaces an existing launcher.
3
+ */
4
+ import { createHash } from 'node:crypto';
5
+ import {
6
+ existsSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ renameSync,
10
+ rmSync,
11
+ writeFileSync,
12
+ } from 'node:fs';
13
+ import { homedir } from 'node:os';
14
+ import { join } from 'node:path';
15
+ import { spawnSync } from 'node:child_process';
16
+ import { workspaceRoot } from './global-config';
17
+ interface Configuration {
18
+ vars: {
19
+ t3_version: string;
20
+ t3_linux_x64_sha256: string;
21
+ t3_linux_arm64_sha256: string;
22
+ t3_desktop_amd64_sha256: string;
23
+ t3_desktop_arm64_sha256: string;
24
+ };
25
+ }
26
+ /** Pinned desktop .deb for Ubuntu workers. The GUI app is the worker's only T3
27
+ * process and embeds the server; the CLI tarball stays for policy commands. */
28
+ export function desktopT3Artifact(config: Configuration, architecture: string) {
29
+ const deb = { x64: 'amd64', arm64: 'arm64' }[architecture];
30
+ if (!deb) throw new Error('Unsupported T3 architecture.');
31
+ const version = config.vars.t3_version;
32
+ const sha256 =
33
+ deb === 'amd64'
34
+ ? config.vars.t3_desktop_amd64_sha256
35
+ : config.vars.t3_desktop_arm64_sha256;
36
+ if (
37
+ !/^\d+\.\d+\.\d+(?:-nightly\.\d{8}\.\d+)?$/.test(version) ||
38
+ !/^[a-f0-9]{64}$/.test(sha256)
39
+ )
40
+ throw new Error('Invalid T3 desktop pin.');
41
+ const name = `T3-Code-${version}-${deb}.deb`;
42
+ return {
43
+ version,
44
+ sha256,
45
+ name,
46
+ url: `https://github.com/pingdotgg/t3code/releases/download/v${version}/${name}`,
47
+ };
48
+ }
49
+ export function managedT3Desktop() {
50
+ const config = Bun.TOML.parse(
51
+ readFileSync(join(workspaceRoot, 'mise.toml'), 'utf8')
52
+ ) as Configuration;
53
+ return desktopT3Artifact(config, process.arch);
54
+ }
55
+ export function nativeT3Artifact(config: Configuration, architecture: string) {
56
+ if (!['x64', 'arm64'].includes(architecture))
57
+ throw new Error('Unsupported T3 architecture.');
58
+ const version = config.vars.t3_version;
59
+ const sha256 =
60
+ architecture === 'x64'
61
+ ? config.vars.t3_linux_x64_sha256
62
+ : config.vars.t3_linux_arm64_sha256;
63
+ if (
64
+ !/^\d+\.\d+\.\d+(?:-nightly\.\d{8}\.\d+)?$/.test(version) ||
65
+ !/^[a-f0-9]{64}$/.test(sha256)
66
+ )
67
+ throw new Error('Invalid T3 pin.');
68
+ const name = `t3-${version}-linux-${architecture}`;
69
+ return {
70
+ version,
71
+ sha256,
72
+ name,
73
+ url: `https://github.com/pingdotgg/t3code/releases/download/v${version}/${name}.tar.gz`,
74
+ };
75
+ }
76
+ export function managedT3(): { path: string; version: string } {
77
+ const config = Bun.TOML.parse(
78
+ readFileSync(join(workspaceRoot, 'mise.toml'), 'utf8')
79
+ ) as Configuration;
80
+ const artifact = nativeT3Artifact(config, process.arch);
81
+ return {
82
+ path: join(homedir(), '.local/share/personal/tools', artifact.name, 't3'),
83
+ version: artifact.version,
84
+ };
85
+ }
86
+ export async function installNativeT3(): Promise<void> {
87
+ if (process.platform !== 'linux') return;
88
+ const config = Bun.TOML.parse(
89
+ readFileSync(join(workspaceRoot, 'mise.toml'), 'utf8')
90
+ ) as Configuration;
91
+ const artifact = nativeT3Artifact(config, process.arch);
92
+ const root = join(homedir(), '.local/share/personal/tools');
93
+ const archive = join(root, `${artifact.name}.tar.gz`);
94
+ mkdirSync(root, { recursive: true, mode: 0o700 });
95
+ if (!existsSync(archive)) {
96
+ const response = await fetch(artifact.url, {
97
+ signal: AbortSignal.timeout(120_000),
98
+ });
99
+ if (!response.ok) throw new Error('Pinned T3 artifact unavailable.');
100
+ const bytes = Buffer.from(await response.arrayBuffer());
101
+ if (createHash('sha256').update(bytes).digest('hex') !== artifact.sha256)
102
+ throw new Error('T3 artifact checksum mismatch.');
103
+ writeFileSync(`${archive}.partial`, bytes, { mode: 0o600 });
104
+ renameSync(`${archive}.partial`, archive);
105
+ }
106
+ if (
107
+ createHash('sha256').update(readFileSync(archive)).digest('hex') !==
108
+ artifact.sha256
109
+ )
110
+ throw new Error(
111
+ 'T3 cached artifact drift; preserve running service and review maintenance recovery.'
112
+ );
113
+ const directory = join(root, artifact.name);
114
+ if (!existsSync(directory)) {
115
+ const staging = join(root, `${artifact.name}.staging`);
116
+ mkdirSync(staging, { recursive: true, mode: 0o700 });
117
+ try {
118
+ if (
119
+ spawnSync(
120
+ 'tar',
121
+ ['--same-permissions', '-xzf', archive, '-C', staging],
122
+ { timeout: 120_000 }
123
+ ).status !== 0
124
+ )
125
+ throw new Error('T3 extraction failed.');
126
+ verifyArchive(archive, staging);
127
+ renameSync(join(staging, artifact.name), directory);
128
+ } finally {
129
+ rmSync(staging, { recursive: true, force: true });
130
+ }
131
+ } else verifyArchive(archive, root);
132
+ const version = spawnSync(join(directory, 't3'), ['--version'], {
133
+ encoding: 'utf8',
134
+ timeout: 30_000,
135
+ });
136
+ if (
137
+ version.status !== 0 ||
138
+ version.stdout.trim() !== `t3 v${artifact.version}`
139
+ )
140
+ throw new Error('Pinned T3 executable verification failed.');
141
+ }
142
+
143
+ function verifyArchive(archive: string, root: string): void {
144
+ if (
145
+ spawnSync(
146
+ 'python3',
147
+ [join(workspaceRoot, 'bootstrap/verify-t3-archive.py'), archive, root],
148
+ { timeout: 120_000 }
149
+ ).status !== 0
150
+ )
151
+ throw new Error(
152
+ 'Managed T3 files differ from the pinned artifact; maintenance is required.'
153
+ );
154
+ }