@crvouga/dev-fleet 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (363) hide show
  1. package/.mise/locks/aws-cdk/2.1146.1/aube-lock.yaml +24 -0
  2. package/.mise/locks/aws-cdk/2.1146.1/package.json +7 -0
  3. package/.mise/locks/azure-cli/2.91.0~a837e014/pyproject.toml +5 -0
  4. package/.mise/locks/azure-cli/2.91.0~a837e014/uv.lock +2794 -0
  5. package/.mise/locks/gemini/0.66.0~84aac8e6/aube-lock.yaml +123 -0
  6. package/.mise/locks/gemini/0.66.0~84aac8e6/package.json +12 -0
  7. package/.mise/locks/npm-devcontainers-cli/0.90.0/aube-lock.yaml +24 -0
  8. package/.mise/locks/npm-devcontainers-cli/0.90.0/package.json +7 -0
  9. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/aube-lock.yaml +1432 -0
  10. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/package.json +7 -0
  11. package/.mise/locks/npm-vercel/65.8.0/aube-lock.yaml +3595 -0
  12. package/.mise/locks/npm-vercel/65.8.0/package.json +7 -0
  13. package/.mise/locks/npm-wrangler/4.156.0/aube-lock.yaml +895 -0
  14. package/.mise/locks/npm-wrangler/4.156.0/package.json +7 -0
  15. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/aube-lock.yaml +24 -0
  16. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/package.json +7 -0
  17. package/LICENSE +21 -0
  18. package/README.md +357 -2
  19. package/agents/antigravity-proxy.ts +3 -0
  20. package/agents/audit.md +89 -0
  21. package/agents/claude-proxy +5 -0
  22. package/agents/claude-proxy.ts +3 -0
  23. package/agents/codex-proxy.ts +3 -0
  24. package/agents/grok-proxy.ts +3 -0
  25. package/agents/managed-proxy.ts +55 -0
  26. package/agents/opencode-proxy.ts +3 -0
  27. package/agents/registry.json +209 -0
  28. package/agents/registry.schema.json +149 -0
  29. package/agents/setup.md +158 -0
  30. package/agents/t3.json +40 -0
  31. package/ansible/macos/manager.yml +30 -0
  32. package/ansible/macos/policy.json +9 -0
  33. package/ansible/monitoring/README.md +215 -0
  34. package/ansible/monitoring/agent-darwin.yml +198 -0
  35. package/ansible/monitoring/agent.yml +172 -0
  36. package/ansible/monitoring/beszel-agent.service +42 -0
  37. package/ansible/monitoring/compose.yml +74 -0
  38. package/ansible/monitoring/hub-darwin.yml +102 -0
  39. package/ansible/monitoring/hub-route-darwin.yml +51 -0
  40. package/ansible/monitoring/lock.json +16 -0
  41. package/ansible/net/remote-probe.sh +13 -0
  42. package/ansible/pyproject.toml +12 -0
  43. package/ansible/ubuntu/authorize-sudo.sh +51 -0
  44. package/ansible/ubuntu/baseline.yml +89 -0
  45. package/ansible/ubuntu/disk-gc.py +336 -0
  46. package/ansible/ubuntu/disk-guard.py +104 -0
  47. package/ansible/ubuntu/policy.json +52 -0
  48. package/ansible/ubuntu/recovery.yml +15 -0
  49. package/ansible/ubuntu/t3/policy.json +7 -0
  50. package/ansible/ubuntu/t3-desktop.yml +220 -0
  51. package/ansible/ubuntu/t3-health.sh +33 -0
  52. package/ansible/ubuntu/t3-policy.yml +76 -0
  53. package/ansible/ubuntu/t3-route.yml +28 -0
  54. package/ansible/ubuntu/tailscale.yml +78 -0
  55. package/ansible/ubuntu/tasks/credentials.yml +47 -0
  56. package/ansible/ubuntu/tasks/docker.yml +31 -0
  57. package/ansible/ubuntu/tasks/packages.yml +56 -0
  58. package/ansible/ubuntu/tasks/power.yml +51 -0
  59. package/ansible/ubuntu/tasks/recovery.yml +39 -0
  60. package/ansible/ubuntu/tasks/resources.yml +312 -0
  61. package/ansible/ubuntu/tasks/services.yml +31 -0
  62. package/ansible/ubuntu/tasks/ssh.yml +71 -0
  63. package/ansible/ubuntu/tasks/user.yml +120 -0
  64. package/ansible/uv.lock +190 -0
  65. package/bin/devfleet.mjs +36 -0
  66. package/bootstrap/bootstrap.ps1 +50 -0
  67. package/bootstrap/bootstrap.sh +80 -0
  68. package/bootstrap/devfleet +7 -0
  69. package/bootstrap/docker-desktop.sh +33 -0
  70. package/bootstrap/doctor.ts +234 -0
  71. package/bootstrap/fleet-manager-run.sh +103 -0
  72. package/bootstrap/fleet-remote-bootstrap.sh +258 -0
  73. package/bootstrap/global-config.ts +171 -0
  74. package/bootstrap/install-launcher.sh +10 -0
  75. package/bootstrap/native-aws.ts +86 -0
  76. package/bootstrap/native-t3.ts +154 -0
  77. package/bootstrap/npm.ts +358 -0
  78. package/bootstrap/release-version.ts +157 -0
  79. package/bootstrap/setup-status.ts +51 -0
  80. package/bootstrap/setup.ts +88 -0
  81. package/bootstrap/shell-path.ts +105 -0
  82. package/bootstrap/verify-t3-archive.py +49 -0
  83. package/bootstrap/worker-auth.ts +65 -0
  84. package/bootstrap/worker-github.ts +47 -0
  85. package/bootstrap/worker-tools.ts +24 -0
  86. package/cliproxy/AGENTS.md +11 -0
  87. package/cliproxy/README.md +269 -0
  88. package/cliproxy/clients/bootstrap.py +143 -0
  89. package/cliproxy/clients/launcher.py +57 -0
  90. package/cliproxy/compose.yaml +90 -0
  91. package/cliproxy/config.template.yaml +70 -0
  92. package/cliproxy/docs/cloud-adoption.md +16 -0
  93. package/cliproxy/docs/deployment.png +0 -0
  94. package/cliproxy/docs/deployment.svg +109 -0
  95. package/cliproxy/docs/health-workflow.example.yml +185 -0
  96. package/cliproxy/docs/migration.md +131 -0
  97. package/cliproxy/docs/model-health-report.schema.json +364 -0
  98. package/cliproxy/docs/model-health.md +393 -0
  99. package/cliproxy/image.lock.json +5 -0
  100. package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
  101. package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
  102. package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
  103. package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
  104. package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
  105. package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
  106. package/cliproxy/models.json +1618 -0
  107. package/cliproxy/requirements.lock +78 -0
  108. package/cliproxy/scripts/accounts.py +38 -0
  109. package/cliproxy/scripts/audit.py +123 -0
  110. package/cliproxy/scripts/backup +234 -0
  111. package/cliproxy/scripts/check.py +34 -0
  112. package/cliproxy/scripts/checkpoint.py +226 -0
  113. package/cliproxy/scripts/cli.py +64 -0
  114. package/cliproxy/scripts/cliproxy_host.py +257 -0
  115. package/cliproxy/scripts/cloud_state.py +150 -0
  116. package/cliproxy/scripts/container-health +6 -0
  117. package/cliproxy/scripts/docker +24 -0
  118. package/cliproxy/scripts/external-health.py +79 -0
  119. package/cliproxy/scripts/fleet_config.py +43 -0
  120. package/cliproxy/scripts/fleet_sync.py +274 -0
  121. package/cliproxy/scripts/initialize +84 -0
  122. package/cliproxy/scripts/install-dependencies +48 -0
  123. package/cliproxy/scripts/install.py +406 -0
  124. package/cliproxy/scripts/model_health.py +16 -0
  125. package/cliproxy/scripts/modelhealth/__init__.py +5 -0
  126. package/cliproxy/scripts/modelhealth/checks.py +429 -0
  127. package/cliproxy/scripts/modelhealth/cli.py +117 -0
  128. package/cliproxy/scripts/modelhealth/config.py +186 -0
  129. package/cliproxy/scripts/modelhealth/emulator.py +536 -0
  130. package/cliproxy/scripts/modelhealth/expect.py +88 -0
  131. package/cliproxy/scripts/modelhealth/findings.py +183 -0
  132. package/cliproxy/scripts/modelhealth/protocol.py +330 -0
  133. package/cliproxy/scripts/modelhealth/redact.py +28 -0
  134. package/cliproxy/scripts/modelhealth/report.py +150 -0
  135. package/cliproxy/scripts/modelhealth/runner.py +390 -0
  136. package/cliproxy/scripts/modelhealth/scenario.py +131 -0
  137. package/cliproxy/scripts/modelhealth/schema.py +63 -0
  138. package/cliproxy/scripts/modelhealth/simulate.py +111 -0
  139. package/cliproxy/scripts/modelhealth/transport.py +213 -0
  140. package/cliproxy/scripts/monitor +68 -0
  141. package/cliproxy/scripts/ops +527 -0
  142. package/cliproxy/scripts/public-endpoint +127 -0
  143. package/cliproxy/scripts/recover_node.py +177 -0
  144. package/cliproxy/scripts/restore-verify +60 -0
  145. package/cliproxy/scripts/retention.py +144 -0
  146. package/cliproxy/scripts/service-start +7 -0
  147. package/cliproxy/scripts/validate-config +56 -0
  148. package/cliproxy/scripts/vault-access.py +57 -0
  149. package/cliproxy/scripts/vault-backup-login +55 -0
  150. package/cliproxy/scripts/vault-recovery +56 -0
  151. package/cliproxy/scripts/vault-renew +30 -0
  152. package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
  153. package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
  154. package/cliproxy/systemd/cliproxy-backup.service +13 -0
  155. package/cliproxy/systemd/cliproxy-backup.timer +8 -0
  156. package/cliproxy/systemd/cliproxy-fleet.service +17 -0
  157. package/cliproxy/systemd/cliproxy-gc.service +13 -0
  158. package/cliproxy/systemd/cliproxy-gc.timer +8 -0
  159. package/cliproxy/systemd/cliproxy-monitor.service +8 -0
  160. package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
  161. package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
  162. package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
  163. package/cliproxy/vendor/LICENSE +22 -0
  164. package/cliproxy/vendor/config.upstream.yaml +1349 -0
  165. package/docs/consumer-setup.md +408 -0
  166. package/docs/declared.md +89 -0
  167. package/docs/machine-setup.md +399 -0
  168. package/docs/operations.md +276 -0
  169. package/docs/releasing.md +103 -0
  170. package/docs/state.md +254 -0
  171. package/docs/topology.md +67 -0
  172. package/docs/ubuntu-sync.md +351 -0
  173. package/docs/worker-contract.md +67 -0
  174. package/examples/fleet.json +100 -0
  175. package/examples/roster.json +13 -0
  176. package/llms.txt +35 -0
  177. package/mise.lock +1521 -0
  178. package/mise.toml +110 -0
  179. package/package.json +74 -4
  180. package/schema/fleet.schema.json +420 -0
  181. package/schema/roster.schema.json +77 -0
  182. package/src/agents/account-relay.ts +111 -0
  183. package/src/agents/accounts.ts +85 -0
  184. package/src/agents/audit.ts +77 -0
  185. package/src/agents/cli.ts +13 -0
  186. package/src/agents/command.ts +204 -0
  187. package/src/agents/converge.ts +123 -0
  188. package/src/agents/doctor.ts +107 -0
  189. package/src/agents/errors.ts +2 -0
  190. package/src/agents/files.ts +73 -0
  191. package/src/agents/fleet-verify.ts +165 -0
  192. package/src/agents/launch.ts +290 -0
  193. package/src/agents/native-verify.ts +371 -0
  194. package/src/agents/provider-errors.ts +57 -0
  195. package/src/agents/registry.ts +61 -0
  196. package/src/agents/render.ts +110 -0
  197. package/src/agents/round-trip.ts +192 -0
  198. package/src/agents/secrets.ts +13 -0
  199. package/src/agents/toolchain.ts +34 -0
  200. package/src/agents/verify-cache.ts +103 -0
  201. package/src/agents/verify.ts +127 -0
  202. package/src/auth-checks.ts +163 -0
  203. package/src/cli/agent-guide.ts +129 -0
  204. package/src/cli/app.ts +122 -0
  205. package/src/cli/cli.ts +28 -0
  206. package/src/cli/command-index.ts +57 -0
  207. package/src/cli/contracts.ts +25 -0
  208. package/src/cli/fleet-reference.ts +17 -0
  209. package/src/cli/fleet-state.ts +499 -0
  210. package/src/cli/fleet-sync-checkout.ts +130 -0
  211. package/src/cli/fleet-sync-enrollment.ts +140 -0
  212. package/src/cli/fleet-sync-forward.ts +134 -0
  213. package/src/cli/fleet-sync-manager.ts +249 -0
  214. package/src/cli/fleet-sync-monitoring.ts +185 -0
  215. package/src/cli/fleet-sync-output.ts +317 -0
  216. package/src/cli/fleet-sync-privilege.ts +27 -0
  217. package/src/cli/fleet-sync-progress.ts +109 -0
  218. package/src/cli/fleet-sync-remote.ts +242 -0
  219. package/src/cli/fleet-sync-report.ts +26 -0
  220. package/src/cli/fleet-sync-schedule.ts +138 -0
  221. package/src/cli/fleet-sync-state.ts +86 -0
  222. package/src/cli/fleet-sync-vault.ts +166 -0
  223. package/src/cli/fleet-sync.ts +375 -0
  224. package/src/cli/fleet.ts +165 -0
  225. package/src/cli/managed-health.ts +219 -0
  226. package/src/cli/node-deployment.ts +34 -0
  227. package/src/cli/node-sync.ts +121 -0
  228. package/src/cli/node-t3.ts +31 -0
  229. package/src/cli/options.ts +141 -0
  230. package/src/cli/probe-source.ts +7 -0
  231. package/src/cli/program.ts +185 -0
  232. package/src/cli/project.ts +51 -0
  233. package/src/cli/register-fleet.ts +296 -0
  234. package/src/cli/registry.ts +185 -0
  235. package/src/cli/repair-apply.ts +67 -0
  236. package/src/cli/repair-guard.ts +11 -0
  237. package/src/cli/repair-inspect.ts +150 -0
  238. package/src/cli/repair-script.ts +76 -0
  239. package/src/cli/repair.ts +209 -0
  240. package/src/cli/roster.ts +105 -0
  241. package/src/cli/search.ts +35 -0
  242. package/src/cli/settings.ts +67 -0
  243. package/src/cli/source-operations.ts +102 -0
  244. package/src/cli/t3/command.ts +90 -0
  245. package/src/cli/t3/register.ts +55 -0
  246. package/src/cli/tailnet.ts +127 -0
  247. package/src/cli/ui-actions.ts +133 -0
  248. package/src/cli/ui-controller.ts +113 -0
  249. package/src/cli/ui-data.ts +106 -0
  250. package/src/cli/ui-execute.ts +163 -0
  251. package/src/cli/ui-repairs.ts +34 -0
  252. package/src/cli/ui-reports.ts +211 -0
  253. package/src/cli/ui-targets.ts +100 -0
  254. package/src/cli/ui-terminal.ts +109 -0
  255. package/src/cli/ui-types.ts +49 -0
  256. package/src/cli/ui-wizard.ts +101 -0
  257. package/src/cli/ui.ts +9 -0
  258. package/src/cli/worker-health-output.ts +37 -0
  259. package/src/config.ts +186 -0
  260. package/src/configure.ts +255 -0
  261. package/src/definition.ts +290 -0
  262. package/src/diagnostics.ts +92 -0
  263. package/src/doctor-check.ts +10 -0
  264. package/src/errors.ts +1 -0
  265. package/src/fingerprint.ts +120 -0
  266. package/src/fleet-config.ts +174 -0
  267. package/src/input-hashes.ts +73 -0
  268. package/src/machine/local-source.ts +52 -0
  269. package/src/machine/lock-error.ts +2 -0
  270. package/src/machine/platform.ts +44 -0
  271. package/src/machine/source.ts +68 -0
  272. package/src/machine/state.ts +173 -0
  273. package/src/machine-health.ts +95 -0
  274. package/src/managed-source.ts +47 -0
  275. package/src/manager-failure.ts +48 -0
  276. package/src/manager-sync.ts +405 -0
  277. package/src/manager.ts +62 -0
  278. package/src/monitoring/agent-evidence.ts +146 -0
  279. package/src/monitoring/apply.ts +183 -0
  280. package/src/monitoring/beszel.ts +198 -0
  281. package/src/monitoring/enroll.ts +111 -0
  282. package/src/monitoring/hub-access.ts +114 -0
  283. package/src/monitoring/hub-backup.ts +121 -0
  284. package/src/monitoring/hub-viewer.ts +142 -0
  285. package/src/monitoring/hub.ts +93 -0
  286. package/src/monitoring/manager-agent.ts +73 -0
  287. package/src/monitoring/manager-hub.ts +248 -0
  288. package/src/monitoring/remote.ts +65 -0
  289. package/src/network-definition.ts +83 -0
  290. package/src/node-converge-linux.ts +374 -0
  291. package/src/node-converge-shared.ts +83 -0
  292. package/src/node-converge.ts +40 -0
  293. package/src/onboard.ts +35 -0
  294. package/src/package-info.ts +45 -0
  295. package/src/platform/ansible-receipt.ts +128 -0
  296. package/src/platform/ansible.ts +66 -0
  297. package/src/platform/darwin.ts +203 -0
  298. package/src/platform/detect.ts +11 -0
  299. package/src/platform/fallback.ts +110 -0
  300. package/src/platform/index.ts +38 -0
  301. package/src/platform/launchd.ts +98 -0
  302. package/src/platform/linux.ts +139 -0
  303. package/src/platform/ps.ts +45 -0
  304. package/src/platform/resource-verify.ts +132 -0
  305. package/src/platform/systemd-user.ts +95 -0
  306. package/src/platform/t3-policy.ts +137 -0
  307. package/src/platform/t3-route.ts +68 -0
  308. package/src/platform/t3-service.ts +75 -0
  309. package/src/platform/types.ts +123 -0
  310. package/src/platform/ubuntu-audit.ts +66 -0
  311. package/src/platform/ubuntu-verify.ts +105 -0
  312. package/src/platform/windows.ts +148 -0
  313. package/src/power-recovery.ts +55 -0
  314. package/src/probe.ts +51 -0
  315. package/src/protocol.ts +119 -0
  316. package/src/proxy-converge.ts +85 -0
  317. package/src/remote-bootstrap.ts +210 -0
  318. package/src/remote-entry.ts +89 -0
  319. package/src/remote-lock.ts +12 -0
  320. package/src/repair-catalog.ts +206 -0
  321. package/src/repair-plan.ts +78 -0
  322. package/src/repair-runner.ts +84 -0
  323. package/src/report.ts +71 -0
  324. package/src/runner.ts +104 -0
  325. package/src/secrets.ts +169 -0
  326. package/src/service-auth.ts +115 -0
  327. package/src/ssh.ts +135 -0
  328. package/src/state/bucket.ts +208 -0
  329. package/src/state/check.ts +74 -0
  330. package/src/state/connection.ts +92 -0
  331. package/src/state/enrollment.ts +45 -0
  332. package/src/state/legacy.ts +89 -0
  333. package/src/state/litestream.ts +143 -0
  334. package/src/state/operator.ts +20 -0
  335. package/src/state/paths.ts +40 -0
  336. package/src/state/roster.ts +62 -0
  337. package/src/state/store.ts +169 -0
  338. package/src/state/sync.ts +352 -0
  339. package/src/sync.ts +104 -0
  340. package/src/t3/accounts.ts +81 -0
  341. package/src/t3/desired.ts +165 -0
  342. package/src/t3/files.ts +81 -0
  343. package/src/t3/names-check.ts +53 -0
  344. package/src/t3/names.ts +155 -0
  345. package/src/t3/plan.ts +82 -0
  346. package/src/t3/providers.ts +74 -0
  347. package/src/t3/reconcile.ts +123 -0
  348. package/src/t3-endpoint.ts +104 -0
  349. package/src/t3-linked-check.ts +123 -0
  350. package/src/tailnet-health.ts +231 -0
  351. package/src/tailnet-matching.ts +37 -0
  352. package/src/tailscale.ts +141 -0
  353. package/src/toolchain-checks.ts +108 -0
  354. package/src/topology.ts +24 -0
  355. package/src/transport.ts +122 -0
  356. package/src/types.ts +99 -0
  357. package/src/vault-config.ts +38 -0
  358. package/src/worker-access-checks.ts +168 -0
  359. package/src/worker-capacity.ts +34 -0
  360. package/src/worker-host-checks.ts +181 -0
  361. package/src/worker-policy.ts +64 -0
  362. package/src/worker-readiness.ts +64 -0
  363. package/src/workstation-checks.ts +29 -0
package/src/t3/plan.ts ADDED
@@ -0,0 +1,82 @@
1
+ import { FleetUsageError } from '../errors';
2
+ import { canonical, hash, object, read } from './files';
3
+
4
+ export type Change = {
5
+ id: string;
6
+ action: 'create' | 'update' | 'delete' | 'unchanged' | 'conflict';
7
+ };
8
+ export type T3Paths = { settings: string; state: string };
9
+
10
+ export async function planT3(paths: T3Paths, desired: Record<string, unknown>) {
11
+ const raw = await read(paths.settings);
12
+ const stateRaw = await read(paths.state);
13
+ const settings = object(raw);
14
+ const current = object(JSON.stringify(settings['providerInstances'] ?? {}));
15
+ const owned = readOwnership(stateRaw, paths.settings);
16
+ const next = { ...current };
17
+ const changes: Change[] = [];
18
+ for (const [id, instance] of Object.entries(desired)) {
19
+ const exists = Object.hasOwn(current, id);
20
+ const action = actionFor(exists, owned.includes(id), current[id], instance);
21
+ changes.push({ id, action });
22
+ if (action !== 'conflict') next[id] = instance;
23
+ }
24
+ for (const id of owned.filter((id) => !Object.hasOwn(desired, id))) {
25
+ changes.push({
26
+ id,
27
+ action: Object.hasOwn(current, id) ? 'delete' : 'unchanged',
28
+ });
29
+ delete next[id];
30
+ }
31
+ const settingsChanged = canonical(next) !== canonical(current);
32
+ const state = ownership(paths.settings, Object.keys(desired));
33
+ return {
34
+ raw,
35
+ stateRaw,
36
+ settings: settingsChanged
37
+ ? { ...settings, providerInstances: next }
38
+ : settings,
39
+ state,
40
+ settingsChanged,
41
+ stateChanged: canonical(object(stateRaw)) !== canonical(state),
42
+ changes,
43
+ planHash: hash(
44
+ canonical({ target: paths.settings, raw, stateRaw, desired })
45
+ ),
46
+ };
47
+ }
48
+
49
+ export function ownership(target: string, ids: string[]) {
50
+ return { version: 1, target, instances: [...ids].sort() };
51
+ }
52
+
53
+ export function readOwnership(raw: string | null, target: string): string[] {
54
+ if (raw === null) return [];
55
+ const state = object(raw);
56
+ const ids = state['instances'];
57
+ if (
58
+ state['version'] !== 1 ||
59
+ state['target'] !== target ||
60
+ !Array.isArray(ids) ||
61
+ ids.some(
62
+ (id) =>
63
+ typeof id !== 'string' || !/^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/.test(id)
64
+ ) ||
65
+ new Set(ids).size !== ids.length
66
+ )
67
+ throw new FleetUsageError(
68
+ 'Invalid T3 ownership state; restore its backup before reconciliation.'
69
+ );
70
+ return ids as string[];
71
+ }
72
+
73
+ function actionFor(
74
+ exists: boolean,
75
+ owned: boolean,
76
+ current: unknown,
77
+ desired: unknown
78
+ ): Change['action'] {
79
+ if (exists && !owned) return 'conflict';
80
+ if (!exists) return 'create';
81
+ return canonical(current) === canonical(desired) ? 'unchanged' : 'update';
82
+ }
@@ -0,0 +1,74 @@
1
+ import type { Check } from '../types';
2
+ import { desired, nodeId, paths } from './desired';
3
+ import type { Roster } from './accounts';
4
+ import { reconcileT3 } from './reconcile';
5
+ import { stablePath, stableWorkspace } from '../platform/t3-policy';
6
+
7
+ /** Every machine's T3 settings hold exactly the managed provider instances:
8
+ * the fixed ones plus one per account signed in on the CLI proxy. T3 watches
9
+ * its settings, so providers appear (and retired ones disappear) at once,
10
+ * without a restart. Workers additionally lock the same instances through T3
11
+ * administrator policy. The ownership-safe t3-apply engine preserves unrelated
12
+ * settings and unmanaged instances; an existing unowned instance of the same
13
+ * ID is reported, never overwritten.
14
+ */
15
+ export async function machineProviders(
16
+ id: string,
17
+ workspace: string,
18
+ apply: boolean,
19
+ options: { t3Home?: string; roster?: Roster } = {}
20
+ ): Promise<Check> {
21
+ const remedy = {
22
+ action:
23
+ 'Run devfleet t3-apply --dry-run on this machine and review any conflicting instance; adopt it with t3-apply --import ID only if it should be managed.',
24
+ };
25
+ try {
26
+ const target = paths(options, workspace);
27
+ // Providers launch from the stable path of the applied run checkout, not
28
+ // from this revision's own directory, which a later sync prunes.
29
+ const home = apply
30
+ ? await stableWorkspace(workspace)
31
+ : stablePath(workspace);
32
+ const instances = await desired(
33
+ target.definition,
34
+ home,
35
+ nodeId(id),
36
+ options.roster
37
+ );
38
+ const plan = await reconcileT3(target, instances, { apply: false });
39
+ const result =
40
+ apply && plan.ok && plan.changed
41
+ ? await reconcileT3(target, instances, {
42
+ apply: true,
43
+ planHash: plan.planHash,
44
+ })
45
+ : plan;
46
+ if (!result.ok)
47
+ return {
48
+ id: 't3:providers',
49
+ status: 'fail',
50
+ summary: 'T3_PROVIDER_CONFLICT',
51
+ remedy,
52
+ };
53
+ return result.changed && !apply
54
+ ? {
55
+ id: 't3:providers',
56
+ status: 'warn',
57
+ summary: 'T3_PROVIDERS_PENDING',
58
+ remedy,
59
+ }
60
+ : {
61
+ id: 't3:providers',
62
+ status: 'pass',
63
+ summary:
64
+ 'T3 on this machine offers exactly the managed provider instances every machine gets.',
65
+ };
66
+ } catch {
67
+ return {
68
+ id: 't3:providers',
69
+ status: 'fail',
70
+ summary: 'T3_PROVIDERS_UNVERIFIED',
71
+ remedy,
72
+ };
73
+ }
74
+ }
@@ -0,0 +1,123 @@
1
+ import { mkdir, rm } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ import { FleetUsageError } from '../errors';
4
+ import { canonical, object, read, safePath, write } from './files';
5
+ import { ownership, planT3, readOwnership, type T3Paths } from './plan';
6
+
7
+ export async function reconcileT3(
8
+ paths: T3Paths,
9
+ desired: Record<string, unknown>,
10
+ options: { apply: boolean; planHash?: string }
11
+ ) {
12
+ if (!options.apply) return report(await planT3(paths, desired), false);
13
+ return locked(paths, async () => {
14
+ const plan = await planT3(paths, desired);
15
+ if (options.planHash && options.planHash !== plan.planHash)
16
+ throw new FleetUsageError(
17
+ 'T3 plan is stale. Run devfleet t3-apply --dry-run again.'
18
+ );
19
+ if (plan.changes.some((change) => change.action === 'conflict'))
20
+ throw new FleetUsageError(
21
+ 'T3 instance already exists without ownership. Review it and use devfleet t3-apply --import ID.'
22
+ );
23
+ if (!plan.settingsChanged && !plan.stateChanged) return report(plan, true);
24
+ await persist(paths, desired, plan);
25
+ const verified = await planT3(paths, desired);
26
+ if (verified.settingsChanged || verified.stateChanged)
27
+ throw new FleetUsageError(
28
+ 'T3 changed after apply; reconciliation is incomplete. Replan and retry.'
29
+ );
30
+ return report(plan, true);
31
+ });
32
+ }
33
+
34
+ export async function importT3(paths: T3Paths, id: string) {
35
+ return locked(paths, async () => {
36
+ const raw = await read(paths.settings);
37
+ const instances = object(
38
+ JSON.stringify(object(raw)['providerInstances'] ?? {})
39
+ );
40
+ if (!Object.hasOwn(instances, id))
41
+ throw new FleetUsageError('T3 import requires an existing instance ID.');
42
+ const state = await read(paths.state);
43
+ const owned = readOwnership(state, paths.settings);
44
+ if (!owned.includes(id)) {
45
+ if (state !== null) await write(`${paths.state}.backup`, state);
46
+ if ((await read(paths.settings)) !== raw)
47
+ throw new FleetUsageError('T3 changed during import; retry.');
48
+ await write(
49
+ paths.state,
50
+ canonical(ownership(paths.settings, [...owned, id])) + '\n'
51
+ );
52
+ }
53
+ return { version: 1, ok: true, imported: id, changed: !owned.includes(id) };
54
+ });
55
+ }
56
+
57
+ async function locked<T>(paths: T3Paths, action: () => Promise<T>): Promise<T> {
58
+ await safePath(paths.state);
59
+ await mkdir(dirname(paths.state), { recursive: true, mode: 0o700 });
60
+ const lock = `${paths.state}.lock`;
61
+ try {
62
+ await mkdir(lock, { mode: 0o700 });
63
+ } catch {
64
+ throw new FleetUsageError(
65
+ 'Another personal T3 operation holds the lock. Inspect interrupted operations before removing a stale lock.'
66
+ );
67
+ }
68
+ try {
69
+ return await action();
70
+ } finally {
71
+ await rm(lock, { recursive: true });
72
+ }
73
+ }
74
+
75
+ function report(plan: Awaited<ReturnType<typeof planT3>>, applied: boolean) {
76
+ return {
77
+ version: 1,
78
+ ok: !plan.changes.some((change) => change.action === 'conflict'),
79
+ applied,
80
+ changed: plan.settingsChanged || plan.stateChanged,
81
+ planHash: plan.planHash,
82
+ changes: plan.changes,
83
+ runtime:
84
+ 'T3 watches settings.json; new sessions use the reconciled instances. Active sessions are not restarted.',
85
+ };
86
+ }
87
+
88
+ async function persist(
89
+ paths: T3Paths,
90
+ desired: Record<string, unknown>,
91
+ plan: Awaited<ReturnType<typeof planT3>>
92
+ ) {
93
+ // Keep a private recovery copy. Contents, environment and secret references never enter reports.
94
+ if (plan.raw !== null && plan.settingsChanged)
95
+ await write(`${paths.state}.settings-backup`, plan.raw);
96
+ if (plan.stateRaw !== null)
97
+ await write(`${paths.state}.backup`, plan.stateRaw);
98
+ if (
99
+ (await read(paths.settings)) !== plan.raw ||
100
+ (await read(paths.state)) !== plan.stateRaw
101
+ )
102
+ throw new FleetUsageError(
103
+ 'T3 configuration changed during apply. Replan and retry.'
104
+ );
105
+ // Intent first: retry after interruption can finish creates/deletes without importing them.
106
+ const previous = readOwnership(plan.stateRaw, paths.settings);
107
+ await write(
108
+ paths.state,
109
+ canonical(
110
+ ownership(paths.settings, [
111
+ ...new Set([...previous, ...Object.keys(desired)]),
112
+ ])
113
+ ) + '\n'
114
+ );
115
+ if (plan.settingsChanged) {
116
+ if ((await read(paths.settings)) !== plan.raw)
117
+ throw new FleetUsageError(
118
+ 'T3 changed during apply. Ownership intent was saved; replan and retry.'
119
+ );
120
+ await write(paths.settings, JSON.stringify(plan.settings, null, 2) + '\n');
121
+ }
122
+ await write(paths.state, canonical(plan.state) + '\n');
123
+ }
@@ -0,0 +1,104 @@
1
+ import { discoverTailnet } from './tailscale';
2
+ import type { Check, FleetNode, Runner } from './types';
3
+
4
+ /** An origin no browser of the fleet uses: the private route must not let
5
+ * a page served from it read the worker's T3.
6
+ */
7
+ export const FOREIGN_ORIGIN = 'https://origin-check.invalid';
8
+
9
+ /** A fresh manager HTTPS request is transport evidence only. Never read
10
+ * pairing material or treat HTTP health as an authenticated tool session.
11
+ * A second request, from a foreign origin, must not be granted cross-origin
12
+ * access.
13
+ */
14
+ export async function verifyT3Endpoint(
15
+ node: FleetNode,
16
+ run: Runner,
17
+ request: (url: string, options: RequestInit) => Promise<Response> = fetch
18
+ ): Promise<Check[]> {
19
+ const tailnet = await discoverTailnet(run);
20
+ const peer = tailnet.computers.find((entry) => entry.id === node.tailscaleId);
21
+ if (!peer || !/^[a-z0-9.-]+\.ts\.net$/.test(peer.dns))
22
+ return pending('T3_MANAGER_IDENTITY_UNVERIFIED');
23
+ try {
24
+ const response = await request('https://' + peer.dns, {
25
+ method: 'GET',
26
+ redirect: 'manual',
27
+ signal: AbortSignal.timeout(10_000),
28
+ });
29
+ await response.body?.cancel();
30
+ if (response.status !== 200) return pending('T3_MANAGER_ENDPOINT_REJECTED');
31
+ } catch {
32
+ return pending('T3_MANAGER_ENDPOINT_UNREACHABLE');
33
+ }
34
+ return [
35
+ {
36
+ id: 't3:manager-endpoint',
37
+ status: 'pass',
38
+ summary:
39
+ 'A fresh HTTPS request from the manager reached the enrolled private endpoint with valid TLS and HTTP 200. Pairing, server compatibility and tool execution remain unverified.',
40
+ },
41
+ await originCheck('https://' + peer.dns, request),
42
+ ];
43
+ }
44
+
45
+ /** Grants cross-origin reads: it echoes the foreign origin, or allows any
46
+ * origin together with credentials.
47
+ */
48
+ function permissive(headers: Headers): boolean {
49
+ const allowed = headers.get('access-control-allow-origin')?.trim();
50
+ return (
51
+ allowed === FOREIGN_ORIGIN ||
52
+ (allowed === '*' &&
53
+ headers.get('access-control-allow-credentials')?.trim() === 'true')
54
+ );
55
+ }
56
+
57
+ async function originCheck(
58
+ url: string,
59
+ request: (url: string, options: RequestInit) => Promise<Response>
60
+ ): Promise<Check> {
61
+ const check = (status: Check['status'], summary: string): Check => ({
62
+ id: 't3:manager-origin',
63
+ status,
64
+ summary,
65
+ ...(status === 'pass'
66
+ ? {}
67
+ : {
68
+ remedy: {
69
+ action:
70
+ 'The private T3 route must not grant cross-origin access to pages from other sites. Restore the stock T3 server and its private Serve route from source; never add a proxy that rewrites CORS headers.',
71
+ },
72
+ }),
73
+ });
74
+ try {
75
+ const response = await request(url, {
76
+ method: 'GET',
77
+ redirect: 'manual',
78
+ headers: { Origin: FOREIGN_ORIGIN },
79
+ signal: AbortSignal.timeout(10_000),
80
+ });
81
+ await response.body?.cancel();
82
+ return permissive(response.headers)
83
+ ? check('fail', 'T3_MANAGER_ORIGIN_UNRESTRICTED')
84
+ : check(
85
+ 'pass',
86
+ 'A request from a foreign origin was not granted cross-origin access to the private T3 route.'
87
+ );
88
+ } catch {
89
+ return check('warn', 'T3_MANAGER_ORIGIN_UNVERIFIED');
90
+ }
91
+ }
92
+ function pending(code: string): Check[] {
93
+ return [
94
+ {
95
+ id: 't3:manager-endpoint',
96
+ status: 'fail',
97
+ summary: code,
98
+ remedy: {
99
+ action:
100
+ 'Inspect the enrolled node’s private Serve route, T3 listener, Tailscale reachability and HTTPS capability. Restore the existing private route in source; never bypass TLS verification or interpret a local listener as manager access.',
101
+ },
102
+ },
103
+ ];
104
+ }
@@ -0,0 +1,123 @@
1
+ import { sshArgs } from './ssh';
2
+ import { record } from './config';
3
+ import type { Check, FleetNode, Runner } from './types';
4
+
5
+ /** Correlate a live SSH-verified server identity with the native manager's authenticated link. */
6
+ export async function verifyT3Linked(
7
+ node: FleetNode,
8
+ run: Runner,
9
+ options: {
10
+ now?: () => number;
11
+ /** After this sync restarted the node's T3, give its link time to reconnect. */
12
+ reconnectMs?: number;
13
+ pause?: () => Promise<void>;
14
+ } = {}
15
+ ): Promise<Check> {
16
+ const now = options.now ?? Date.now;
17
+ const deadline = now() + (options.reconnectMs ?? 0);
18
+ let check = await linkedOnce(node, run, now());
19
+ while (check.status === 'fail' && now() < deadline) {
20
+ await (
21
+ options.pause ?? (() => new Promise((done) => setTimeout(done, 10_000)))
22
+ )();
23
+ check = await linkedOnce(node, run, now());
24
+ }
25
+ return check.status === 'fail' ? explained(node, run, check) : check;
26
+ }
27
+
28
+ /** A worker enrolled with one T3 environment that now runs another lost
29
+ * its identity (a reset data directory, a reinstall): the old link cannot
30
+ * reach it, and a new pairing is the remedy, not a reconnect.
31
+ */
32
+ async function explained(
33
+ node: FleetNode,
34
+ run: Runner,
35
+ check: Check
36
+ ): Promise<Check> {
37
+ if (!node.t3EnvironmentId) return check;
38
+ const observed = await observedIdentity(node, run).catch(() => undefined);
39
+ if (!observed || observed === node.t3EnvironmentId) return check;
40
+ return {
41
+ ...check,
42
+ summary: 'T3_ENVIRONMENT_IDENTITY_CHANGED',
43
+ remedy: {
44
+ action:
45
+ 'This worker now runs a different T3 environment than the one enrolled in the roster, so the manager’s link cannot reach it. Find out why its T3 data was replaced before anything else; then pair the new environment through T3 Connections and enroll it again. Fleet never copies or rewrites a T3 identity.',
46
+ },
47
+ };
48
+ }
49
+
50
+ async function linkedOnce(
51
+ node: FleetNode,
52
+ run: Runner,
53
+ now: number
54
+ ): Promise<Check> {
55
+ const unavailable: Check = {
56
+ id: 't3:paired-access',
57
+ status: 'fail',
58
+ summary: 'MANAGER_T3_LINK_UNVERIFIED',
59
+ remedy: {
60
+ action:
61
+ 'Link this exact worker environment to the manager using T3 Connections once. Existing pairing identities are preserved; passwords and pairing tokens are never copied by Fleet.',
62
+ },
63
+ };
64
+ try {
65
+ const identity =
66
+ node.t3EnvironmentId ?? (await observedIdentity(node, run));
67
+ if (!identity) return unavailable;
68
+ const result = await run(['t3', 'environment', 'list', '--json'], {
69
+ timeoutMs: 30_000,
70
+ });
71
+ const links: unknown = JSON.parse(result.stdout);
72
+ if (!result.ok || !Array.isArray(links)) return unavailable;
73
+ return linkedEvidence(links, identity, unavailable, now);
74
+ } catch {
75
+ return unavailable;
76
+ }
77
+ }
78
+
79
+ function linkedEvidence(
80
+ links: unknown[],
81
+ environmentId: string,
82
+ unavailable: Check,
83
+ now: number
84
+ ): Check {
85
+ const link = links.find(
86
+ (value) => record(value) && value['environmentId'] === environmentId
87
+ );
88
+ if (
89
+ !record(link) ||
90
+ link['status'] !== 'reachable' ||
91
+ typeof link['expiresAt'] !== 'string'
92
+ )
93
+ return unavailable;
94
+ const expires = Date.parse(link['expiresAt']);
95
+ if (!Number.isFinite(expires) || expires <= now) return unavailable;
96
+ return {
97
+ ...unavailable,
98
+ status: expires - now < 7 * 86400000 ? 'warn' : 'pass',
99
+ summary:
100
+ expires - now < 7 * 86400000
101
+ ? 'Exact worker link is reachable but its manager authorization expires within seven days.'
102
+ : 'Native T3 authenticated link reaches the exact enrolled worker environment. This checks connected access, not a model task.',
103
+ };
104
+ }
105
+
106
+ async function observedIdentity(
107
+ node: FleetNode,
108
+ run: Runner
109
+ ): Promise<string | undefined> {
110
+ const server = await run(
111
+ [
112
+ ...sshArgs(node),
113
+ 'curl --fail --silent --max-time 5 http://127.0.0.1:3773/.well-known/t3/environment',
114
+ ],
115
+ { timeoutMs: 15_000 }
116
+ );
117
+ const identity: unknown = JSON.parse(server.stdout);
118
+ return server.ok &&
119
+ record(identity) &&
120
+ typeof identity['environmentId'] === 'string'
121
+ ? identity['environmentId']
122
+ : undefined;
123
+ }