@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,103 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { receiptDirectory } from '../platform/ansible-receipt';
5
+ import type { Check } from '../types';
6
+
7
+ /** A verified inference or agent session is re-proven at least this often. */
8
+ export const VERIFY_MAX_AGE_MS = 6 * 60 * 60 * 1000;
9
+
10
+ export interface VerifyCache {
11
+ directory?: string;
12
+ now?: number;
13
+ /** Ignore cached results (a live probe failed, or a full check was asked). */
14
+ bypass?: boolean;
15
+ }
16
+
17
+ /** Digest of everything a result depends on (never secrets). */
18
+ export function cacheKey(...parts: (string | object)[]): string {
19
+ const hash = createHash('sha256');
20
+ for (const part of parts)
21
+ hash
22
+ .update(typeof part === 'string' ? part : JSON.stringify(part))
23
+ .update('\0');
24
+ return hash.digest('hex');
25
+ }
26
+
27
+ /** Digest of source files, so a code change re-verifies. */
28
+ export async function sourceKey(paths: string[]): Promise<string> {
29
+ const hash = createHash('sha256');
30
+ for (const path of [...paths].sort())
31
+ hash
32
+ .update(path)
33
+ .update('\0')
34
+ .update(await readFile(path).catch(() => ''));
35
+ return hash.digest('hex');
36
+ }
37
+
38
+ /** Warm verification: a passing check with an unchanged key is reused until it
39
+ * ages out. Only clean passes are stored; a failure, a skip or a quota-limited
40
+ * pass (`reuse` false) clears the entry so the next sync checks it again.
41
+ */
42
+ export async function cachedCheck(
43
+ name: string,
44
+ key: string,
45
+ compute: () => Promise<Check>,
46
+ options: VerifyCache & { reuse?: (check: Check) => boolean } = {}
47
+ ): Promise<Check> {
48
+ const directory = options.directory ?? join(receiptDirectory(), 'verify');
49
+ const path = join(directory, `${name.replace(/[^a-z0-9-]/gi, '_')}.json`);
50
+ const now = options.now ?? Date.now();
51
+ if (!options.bypass) {
52
+ const hit = await fresh(path, key, now);
53
+ if (hit) return hit;
54
+ }
55
+ const check = await compute();
56
+ const reusable =
57
+ check.status === 'pass' && (options.reuse ? options.reuse(check) : true);
58
+ try {
59
+ if (!reusable) await rm(path, { force: true });
60
+ else {
61
+ await mkdir(directory, { recursive: true, mode: 0o700 });
62
+ await writeFile(
63
+ path,
64
+ JSON.stringify({ version: 1, key, checkedAt: now, check }) + '\n',
65
+ { mode: 0o600 }
66
+ );
67
+ }
68
+ } catch {
69
+ /* A missing cache entry only means the next sync verifies again. */
70
+ }
71
+ return check;
72
+ }
73
+
74
+ async function fresh(
75
+ path: string,
76
+ key: string,
77
+ now: number
78
+ ): Promise<Check | null> {
79
+ try {
80
+ const entry = JSON.parse(await readFile(path, 'utf8')) as {
81
+ version?: unknown;
82
+ key?: unknown;
83
+ checkedAt?: unknown;
84
+ check?: Check;
85
+ };
86
+ if (
87
+ entry.version !== 1 ||
88
+ entry.key !== key ||
89
+ typeof entry.checkedAt !== 'number' ||
90
+ now - entry.checkedAt < 0 ||
91
+ now - entry.checkedAt >= VERIFY_MAX_AGE_MS ||
92
+ entry.check?.status !== 'pass'
93
+ )
94
+ return null;
95
+ const minutes = Math.round((now - entry.checkedAt) / 60_000);
96
+ return {
97
+ ...entry.check,
98
+ summary: `${entry.check.summary} (verified ${minutes}m ago; re-proven every 6h or on any change)`,
99
+ };
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
@@ -0,0 +1,127 @@
1
+ import {
2
+ ProviderVerificationError,
3
+ providerHttpFailure,
4
+ } from './provider-errors';
5
+ import { registry, type Selection } from './registry';
6
+ import { proxyKey } from './secrets';
7
+ import { VERIFY_OUTPUT_TOKENS } from './round-trip';
8
+
9
+ export async function verifyProvider(
10
+ selection: Selection,
11
+ smoke: boolean,
12
+ credential?: string
13
+ ) {
14
+ if (selection.profile !== 'proxy')
15
+ return {
16
+ static: 'pass',
17
+ reachability: 'unverified',
18
+ authenticatedModels: 'unverified',
19
+ inference: 'unverified',
20
+ streaming: 'unverified',
21
+ toolCalls: 'unverified',
22
+ t3EndToEnd: 'unverified',
23
+ };
24
+ const key = credential ?? (await proxyKey());
25
+ const headers = {
26
+ Authorization: `Bearer ${key}`,
27
+ 'Content-Type': 'application/json',
28
+ };
29
+ const response = await fetch(registry.proxy.endpoint + '/v1/models', {
30
+ headers,
31
+ redirect: 'error',
32
+ signal: AbortSignal.timeout(15_000),
33
+ });
34
+ if (!response.ok) throw providerHttpFailure(response.status);
35
+ const data = (await response.json()) as { data?: Array<{ id: string }> };
36
+ if (!data.data?.some((model) => model.id === selection.model))
37
+ throw new ProviderVerificationError(
38
+ 'PROVIDER_MODEL_UNAVAILABLE',
39
+ 'Approved model is unavailable; review registry model selection.'
40
+ );
41
+ let inference = 'unverified';
42
+ if (smoke) {
43
+ const { path, body } = inferenceRequest(selection);
44
+ const result = await fetch(registry.proxy.endpoint + path, {
45
+ method: 'POST',
46
+ headers: {
47
+ ...headers,
48
+ 'anthropic-version': '2023-06-01',
49
+ 'x-goog-api-key': key,
50
+ },
51
+ body: JSON.stringify(body),
52
+ redirect: 'error',
53
+ signal: AbortSignal.timeout(30_000),
54
+ });
55
+ if (!result.ok) throw providerHttpFailure(result.status);
56
+ await validateOutput(result, selection.tool);
57
+ inference = 'pass';
58
+ }
59
+ return {
60
+ static: 'pass',
61
+ reachability: 'pass',
62
+ authenticatedModels: 'pass',
63
+ inference,
64
+ streaming: 'unverified',
65
+ toolCalls: 'unverified',
66
+ t3EndToEnd: 'unverified',
67
+ };
68
+ }
69
+
70
+ async function validateOutput(
71
+ result: Response,
72
+ tool: Selection['tool']
73
+ ): Promise<void> {
74
+ const payload = (await result.json()) as {
75
+ content?: unknown[];
76
+ output?: unknown[];
77
+ error?: unknown;
78
+ candidates?: unknown[];
79
+ };
80
+ const output =
81
+ tool === 'gemini' || tool === 'antigravity'
82
+ ? payload.candidates
83
+ : tool === 'claude'
84
+ ? payload.content
85
+ : payload.output;
86
+ if (payload.error || !Array.isArray(output) || output.length === 0)
87
+ throw new ProviderVerificationError(
88
+ 'PROVIDER_OUTPUT_UNVERIFIED',
89
+ 'Inference returned no output; inspect the provider locally.'
90
+ );
91
+ }
92
+
93
+ /** Exact pinned clients use different wire protocols; model discovery is not inference.
94
+ * Output uses the same ceiling as the round trip: reasoning models (Muse) spend
95
+ * dozens of tokens before any text, so a tiny cap returns an empty output.
96
+ */
97
+ export function inferenceRequest(selection: Selection): {
98
+ path: string;
99
+ body: object;
100
+ } {
101
+ // Antigravity's CLI speaks the same Gemini generateContent protocol.
102
+ if (selection.tool === 'gemini' || selection.tool === 'antigravity')
103
+ return {
104
+ path: `/v1beta/models/${encodeURIComponent(selection.model)}:generateContent`,
105
+ body: {
106
+ contents: [{ role: 'user', parts: [{ text: 'Reply OK.' }] }],
107
+ generationConfig: { maxOutputTokens: VERIFY_OUTPUT_TOKENS },
108
+ },
109
+ };
110
+ if (selection.tool === 'claude')
111
+ return {
112
+ path: '/v1/messages',
113
+ body: {
114
+ model: selection.model,
115
+ max_tokens: VERIFY_OUTPUT_TOKENS,
116
+ messages: [{ role: 'user', content: 'Reply OK.' }],
117
+ },
118
+ };
119
+ return {
120
+ path: '/v1/responses',
121
+ body: {
122
+ model: selection.model,
123
+ max_output_tokens: VERIFY_OUTPUT_TOKENS,
124
+ input: 'Reply OK.',
125
+ },
126
+ };
127
+ }
@@ -0,0 +1,163 @@
1
+ import { resolveVaultConfig, usesVault } from './vault-config';
2
+ import { fleet as definition } from './fleet-config';
3
+ import { workerReadiness } from './worker-readiness';
4
+ import { record } from './config';
5
+ import { codingAuth, serviceAuth } from './service-auth';
6
+ import { workstationChecks } from './workstation-checks';
7
+ import { toolchainChecks } from './toolchain-checks';
8
+ import type { Capability, Check, Runner, CommandResult } from './types';
9
+
10
+ export async function authCheck(
11
+ capability: Capability,
12
+ run: Runner,
13
+ root: string,
14
+ nodeId?: string
15
+ ): Promise<Check[]> {
16
+ if (capability === 'worker') return workerReadiness(run, nodeId, root);
17
+ if (capability === 't3') return t3Checks(run, nodeId !== undefined);
18
+ if (capability === 'codex' || capability === 'claude')
19
+ return codingAuth(capability, run);
20
+ if (capability === 'services') return serviceAuth(run);
21
+ if (capability === 'workstation') return workstationChecks(root, run);
22
+ if (capability === 'toolchain')
23
+ return toolchainChecks(
24
+ root,
25
+ run,
26
+ definition.workers.some((node) => node.id === nodeId)
27
+ );
28
+ return basicAuth(capability, run, root);
29
+ }
30
+
31
+ async function basicAuth(
32
+ capability: 'github' | 'vault' | 'vscode',
33
+ run: Runner,
34
+ root: string
35
+ ): Promise<Check[]> {
36
+ const spec = {
37
+ github: {
38
+ command: ['gh', 'auth', 'status', '--active'],
39
+ action:
40
+ 'Authenticate on this node with gh auth login, then gh auth setup-git.',
41
+ },
42
+ vault: {
43
+ command: [
44
+ 'vault',
45
+ 'token',
46
+ 'lookup',
47
+ '-format=json',
48
+ `-address=${usesVault() ? resolveVaultConfig().addr : ''}`,
49
+ ],
50
+ action:
51
+ 'Authenticate to the configured Vault on this node. Do not copy a token from another computer.',
52
+ },
53
+ vscode: {
54
+ command: ['code', '--list-extensions'],
55
+ action:
56
+ 'Install the VS Code CLI on PATH and ms-vscode-remote.remote-ssh in the local editor profile.',
57
+ },
58
+ }[capability];
59
+ const result = await run(spec.command, {
60
+ cwd: root,
61
+ timeoutMs: 15_000,
62
+ });
63
+ const extensions = result.stdout.toLowerCase().split(/\s+/);
64
+ const ok =
65
+ result.ok &&
66
+ (capability !== 'vscode' ||
67
+ extensions.includes('ms-vscode-remote.remote-ssh'));
68
+ return [
69
+ {
70
+ id: `auth:${capability}`,
71
+ status: ok ? 'pass' : 'fail',
72
+ summary: ok
73
+ ? `${capability} check succeeded in this process context.`
74
+ : `${capability} check failed (${result.reason}); no credential output retained.`,
75
+ ...(ok ? {} : { remedy: { action: spec.action } }),
76
+ },
77
+ ];
78
+ }
79
+
80
+ async function t3Checks(
81
+ run: Runner,
82
+ managerChecksLinks = false
83
+ ): Promise<Check[]> {
84
+ const t3 = await t3Executable(run);
85
+ const config = await run([t3, 'status', '--json']);
86
+ const checks: Check[] = [t3Settings(config)];
87
+ const live = await run([t3, 'environment', 'list', '--json']);
88
+ checks.push({
89
+ id: 't3:api',
90
+ status: live.ok ? 'pass' : 'fail',
91
+ summary: live.ok
92
+ ? 'Local T3 API answered a read-only environment listing; peer connectivity is not inferred.'
93
+ : 'Local T3 API did not answer the environment listing.',
94
+ ...(!live.ok
95
+ ? {
96
+ remedy: {
97
+ action:
98
+ 'Check t3 service status and t3 status. Inspect linked peers in T3 Settings → Connections; relink only with operator approval.',
99
+ },
100
+ }
101
+ : {}),
102
+ });
103
+ if (!managerChecksLinks)
104
+ checks.push({
105
+ id: 't3:end-to-end',
106
+ status: 'skip',
107
+ summary:
108
+ 'Relay traversal and remote peer authorization require an operator connection test; local settings are not proof.',
109
+ });
110
+ return checks;
111
+ }
112
+
113
+ /** Workers run only the T3 desktop app, so no `t3` launcher need be on PATH.
114
+ * Fall back to the newest pinned native CLI the shared installer verified.
115
+ */
116
+ async function t3Executable(run: Runner): Promise<string> {
117
+ const found = await run(
118
+ [
119
+ 'sh',
120
+ '-c',
121
+ 'command -v t3 || ls -d "$HOME"/.local/share/personal/tools/t3-*/t3 2>/dev/null | sort -V | tail -n 1',
122
+ ],
123
+ { timeoutMs: 5000 }
124
+ );
125
+ const path = found.ok ? (found.stdout.trim().split('\n').pop() ?? '') : '';
126
+ return /^\/[\x21-\x7e]+$/.test(path) ? path : 't3';
127
+ }
128
+
129
+ function t3Settings(config: CommandResult): Check {
130
+ const fallback: Check = {
131
+ id: 't3:settings',
132
+ status: 'fail',
133
+ summary: 'T3 settings unavailable or unsupported CLI response.',
134
+ remedy: {
135
+ action:
136
+ 'Run t3 status --json on this node. Complete t3 up --connect as its operator if needed.',
137
+ },
138
+ };
139
+ try {
140
+ const data: unknown = JSON.parse(config.stdout);
141
+ if (config.ok && record(data) && record(data['connect'])) {
142
+ const enabled =
143
+ data['connect']['enabled'] === true &&
144
+ data['connect']['revoked'] === false;
145
+ return {
146
+ id: 't3:settings',
147
+ status: enabled ? 'pass' : 'fail',
148
+ summary: enabled
149
+ ? 'T3 Connect enabled and not marked revoked (settings only).'
150
+ : 'T3 Connect disabled, revoked, or unknown.',
151
+ ...(!enabled
152
+ ? {
153
+ remedy: {
154
+ action:
155
+ 'Review t3 status, then complete t3 up --connect as the operator. Never share pairing tokens.',
156
+ },
157
+ }
158
+ : {}),
159
+ };
160
+ }
161
+ } catch {}
162
+ return fallback;
163
+ }
@@ -0,0 +1,95 @@
1
+ import type { Release } from './program';
2
+
3
+ const SAFETY = [
4
+ 'No arguments prints help, including on a TTY. --json returns this document. The menu requires --interactive; --json and --non-interactive never prompt.',
5
+ 'Diagnostics, onboarding instructions, schema, catalogs, scripts and repair previews are read-only.',
6
+ 'fleet-sync, fleet-node-sync, t3-apply and t3-cleanup apply by invocation; --dry-run previews. t3-apply --import ID explicitly adopts ownership. --json never implies a dry run. fleet-node-add and fleet-repair require --apply --yes.',
7
+ 'Do not copy credentials, forward SSH agents, bypass host keys, provision infrastructure or execute unreviewed remedies.',
8
+ 'A healthy manager does not prove another machine can connect to it. Run fleet-status from each intended manager.',
9
+ 'fleet-repair executes only allowlisted owner actions, then reruns diagnostics. Native logins, host trust and resource permissions remain explicit operator steps. Pass the reviewed planHash as --plan-hash to reject a changed plan.',
10
+ 'Shared provider keys are fetched through each machine’s existing Vault authorization by workstation tooling. Never copy machine-local login caches.',
11
+ 'Use exact npm versions for automation. The application never self-updates.',
12
+ ];
13
+
14
+ const EXAMPLES = [
15
+ 'devfleet fleet-node-sync',
16
+ 'devfleet fleet-status',
17
+ 'devfleet t3-status',
18
+ 'devfleet fleet-status --local --workspace /path/to/personal --json',
19
+ 'devfleet fleet-status --json --strict --timeout 120000',
20
+ 'devfleet fleet-status --tailnet --json',
21
+ 'devfleet fleet-status --workers --json',
22
+ 'devfleet fleet-node-add --id dev-linux --ssh dev-linux --path /home/dev/workspace --require github,t3,toolchain --json',
23
+ 'devfleet fleet-node-add --id dev-linux --ssh dev-linux --path /home/dev/workspace --apply --yes --json',
24
+ 'devfleet fleet-status --node dev-linux --json',
25
+ 'devfleet fleet-repair --list --json',
26
+ 'devfleet fleet-repair --json',
27
+ 'devfleet fleet-repair --local --repair workspace --json',
28
+ 'devfleet fleet-repair --local --repair workspace --plan-hash <reviewed-sha256> --apply --yes --json',
29
+ ];
30
+
31
+ export function agentGuide(release: Release) {
32
+ return {
33
+ version: 1,
34
+ name: 'dev-fleet',
35
+ release: release.version,
36
+ releasedAt: release.releasedAt,
37
+ invocation: 'devfleet <command> --json --non-interactive',
38
+ discovery: 'devfleet commands --json',
39
+ defaultCommand: 'help',
40
+ help: 'devfleet <command> --help',
41
+ interactive:
42
+ 'devfleet --interactive (explicit opt-in; TTY required, disabled in CI)',
43
+ groups: {
44
+ 'fleet-*':
45
+ 'Developer computers: fleet-status, fleet-discover, fleet-sync, fleet-node-sync, fleet-repair, fleet-nodes, fleet-node-add',
46
+ 't3-*': 'Declared T3 provider instances: t3-status, t3-apply, t3-cleanup',
47
+ cliproxy: 'The CLI proxy service on the manager; arguments are forwarded',
48
+ },
49
+ configuration: {
50
+ workspace:
51
+ '--workspace PATH, PERSONAL_WORKSPACE, closest ancestor mise.toml, then current directory',
52
+ roster:
53
+ '--config PATH, PERSONAL_FLEET_CONFIG, closest ancestor .fleet.local.json, then ~/.config/personal/fleet.json',
54
+ },
55
+ output: {
56
+ stdout:
57
+ 'Public diagnostics emit one versioned JSON document with --json. cliproxy retains its owner output contract; native agent launch commands stream native output.',
58
+ exitCodes: {
59
+ '0': 'Success or preview',
60
+ '1': 'Failed check or operation; strict also rejects warnings/skips',
61
+ '2': 'Invalid usage or configuration',
62
+ '130': 'Cancelled',
63
+ },
64
+ errors:
65
+ '{version:1,ok:false,code,error}; raw subprocess output and exception details are withheld',
66
+ reports:
67
+ '{version:1,command,ok,generatedAt,nodes:[{node,checks:[{id,status,summary,remedy?}]}],summary,coverage}',
68
+ statuses: ['pass', 'warn', 'fail', 'skip'],
69
+ coverage:
70
+ 'Default fleet-status tests the manager and all configured nodes. --local tests only this manager; --tailnet and --workers cover Tailscale computers. A missing roster fails explicitly. T3/VS Code end-to-end checks remain manual.',
71
+ },
72
+ safety: SAFETY,
73
+ fleetSync: {
74
+ command: 'devfleet fleet-sync',
75
+ preview: 'devfleet fleet-sync --dry-run --json',
76
+ approval: 'Applies on invocation without a prompt; --dry-run previews',
77
+ scope:
78
+ 'Whole enrolled Ubuntu fleet over verified SSH; --node filters diagnostics/rollout',
79
+ state: '~/.local/share/personal/fleet/manager/enrollment.json',
80
+ readiness:
81
+ 'Full Ubuntu acceptance is incomplete and fails closed; do not override it or claim deployment success.',
82
+ },
83
+ reconciliation: {
84
+ plan: 'devfleet t3-apply --dry-run --json',
85
+ apply: 'devfleet t3-apply --plan-hash HASH',
86
+ cleanup: 'devfleet t3-cleanup --dry-run',
87
+ import: 'devfleet t3-apply --import ID',
88
+ ownership:
89
+ 'Only declared or explicitly imported T3 instances. Pairing, credentials, threads and unrelated settings are preserved.',
90
+ concurrency:
91
+ 'Exclusive personal lock with optimistic external T3 file updates; avoid concurrent Settings edits.',
92
+ },
93
+ examples: EXAMPLES,
94
+ };
95
+ }
package/src/cli/app.ts ADDED
@@ -0,0 +1,115 @@
1
+ import { sourceOperation, forwardSourceOperation } from './source-operations';
2
+ import { resolveSettings, type Settings } from './settings';
3
+ import { COMMANDS } from './registry';
4
+ import { execute } from './program';
5
+ import { parseArgs } from 'node:util';
6
+ import manifest from '../../package.json';
7
+
8
+ /** The manifest carries `0.0.0-development` in the repository; the release
9
+ * workflow stamps the tag's version into the package it publishes.
10
+ */
11
+ export const VERSION = manifest.version;
12
+ const RELEASE = { version: VERSION, releasedAt: null };
13
+
14
+ export function wantsInteractive(
15
+ args: string[],
16
+ terminal: boolean,
17
+ ci: boolean
18
+ ): boolean {
19
+ if (args.includes('--json') || args.includes('--non-interactive'))
20
+ return false;
21
+ return terminal && !ci && args.includes('--interactive') && contextOnly(args);
22
+ }
23
+
24
+ function startup(args: string[]) {
25
+ return parseArgs({
26
+ args,
27
+ allowPositionals: true,
28
+ strict: false,
29
+ options: {
30
+ workspace: { type: 'string' },
31
+ config: { type: 'string' },
32
+ interactive: { type: 'boolean' },
33
+ json: { type: 'boolean' },
34
+ 'non-interactive': { type: 'boolean' },
35
+ },
36
+ });
37
+ }
38
+
39
+ function contextOnly(args: string[]): boolean {
40
+ const parsed = startup(args);
41
+ return (
42
+ parsed.positionals.length === 0 &&
43
+ Object.keys(parsed.values).every((key) =>
44
+ ['workspace', 'config', 'interactive'].includes(key)
45
+ )
46
+ );
47
+ }
48
+
49
+ export async function main(args = process.argv.slice(2)): Promise<number> {
50
+ const { values } = startup(args);
51
+ const settings = await resolveSettings({
52
+ ...(typeof values.workspace === 'string'
53
+ ? { workspace: values.workspace }
54
+ : {}),
55
+ ...(typeof values.config === 'string' ? { config: values.config } : {}),
56
+ });
57
+ const forwarded = forwardOwner(args, settings);
58
+ if (forwarded) return forwarded;
59
+ const terminal = interactiveTerminal();
60
+ if (args.includes('--interactive') && !terminal) {
61
+ process.stdout.write(
62
+ `${JSON.stringify({ version: 1, ok: false, code: 'tty_required', error: 'Interactive mode needs a terminal. Use personal commands --json or fleet-status --local --json.' })}\n`
63
+ );
64
+ return 2;
65
+ }
66
+ if (wantsInteractive(args, terminal, Boolean(process.env['CI']))) {
67
+ const { startInteractive } = await import('./ui');
68
+ await startInteractive({
69
+ ...settings,
70
+ ...RELEASE,
71
+ commands: COMMANDS,
72
+ execute: (command) => execute(command, settings, RELEASE),
73
+ });
74
+ return 0;
75
+ }
76
+ const outcome = await execute(
77
+ args.filter((arg) => arg !== '--interactive'),
78
+ settings,
79
+ RELEASE
80
+ );
81
+ const text = args.includes('--json')
82
+ ? JSON.stringify(outcome.data, null, 2)
83
+ : (outcome.text ?? JSON.stringify(outcome.data, null, 2));
84
+ process.stdout.write(`${text}\n`);
85
+ return outcome.exitCode;
86
+ }
87
+
88
+ function interactiveTerminal(): boolean {
89
+ return process.stdin.isTTY === true && process.stderr.isTTY === true;
90
+ }
91
+
92
+ function forwardOwner(
93
+ args: string[],
94
+ settings: Settings
95
+ ): Promise<number> | undefined {
96
+ const ownerArgs = stripOwnerWorkspace(args);
97
+ if (sourceOperation(ownerArgs))
98
+ return forwardSourceOperation(ownerArgs, settings);
99
+ return undefined;
100
+ }
101
+
102
+ export function stripOwnerWorkspace(args: string[]): string[] {
103
+ const forwarded: string[] = [];
104
+ let child = false;
105
+ for (let index = 0; index < args.length; index++) {
106
+ const arg = args[index]!;
107
+ if (arg === '--') child = true;
108
+ if (!child && arg === '--workspace') {
109
+ index++;
110
+ continue;
111
+ }
112
+ forwarded.push(arg);
113
+ }
114
+ return forwarded;
115
+ }
package/src/cli/cli.ts ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import { main } from './app';
3
+
4
+ try {
5
+ process.exitCode = await main();
6
+ } catch (error) {
7
+ const cancelled =
8
+ error instanceof Error &&
9
+ ['ExitPromptError', 'AbortPromptError'].includes(error.name);
10
+ process.stdout.write(
11
+ `${JSON.stringify({
12
+ version: 1,
13
+ ok: false,
14
+ code: cancelled ? 'cancelled' : 'operation_failed',
15
+ error: cancelled
16
+ ? 'Cancelled; no further actions will run.'
17
+ : 'Command failed. Inspect the local setup; raw errors are withheld to protect credentials.',
18
+ })}\n`
19
+ );
20
+ process.exitCode = cancelled ? 130 : 1;
21
+ }
@@ -0,0 +1,50 @@
1
+ import { SOURCE_OPERATIONS } from './source-operations';
2
+ import { commandCatalog } from './registry';
3
+ import type { CommandSpec } from './contracts';
4
+
5
+ const APPLIES = new Set(['fleet-node-sync', 't3-apply', 't3-cleanup']);
6
+
7
+ /** Additive machine-readable discovery; the registry also drives the menu. */
8
+ export function indexedCommands(search = '', group?: string) {
9
+ return commandCatalog(search, group).map((entry) => ({
10
+ ...entry,
11
+ help: `devfleet ${entry.args.join(' ')} --help`,
12
+ execution: execution(entry),
13
+ }));
14
+ }
15
+
16
+ function execution(entry: CommandSpec) {
17
+ if (Object.hasOwn(SOURCE_OPERATIONS, entry.id))
18
+ return {
19
+ default: 'depends-on-subcommand',
20
+ authorization: 'owner-specific; inspect leaf help before invocation',
21
+ requirements: ['dev-fleet checkout and its toolchain'],
22
+ note: 'Streams the owner protocol. JSON and mutation flags depend on the leaf operation.',
23
+ };
24
+ if (entry.id === 'fleet-sync')
25
+ return {
26
+ default: 'interactive-confirmation-or-action-required',
27
+ authorization: 'interactive-confirmation or --apply --yes',
28
+ preview: 'devfleet fleet-sync --dry-run --json',
29
+ scope:
30
+ 'enrolled Ubuntu fleet over verified SSH; manager is not an implicit target',
31
+ note: 'JSON/noninteractive never prompt. Full Ubuntu acceptance is not yet implemented.',
32
+ };
33
+ if (APPLIES.has(entry.id))
34
+ return {
35
+ default: 'apply',
36
+ authorization: 'command-invocation',
37
+ preview: `devfleet ${entry.id} --dry-run --json`,
38
+ note:
39
+ entry.id === 't3-apply'
40
+ ? '--import ID adopts an existing instance without reconciling. JSON selects output format; it does not prevent writes.'
41
+ : 'JSON selects output format; it does not prevent writes.',
42
+ };
43
+ if (entry.mutation)
44
+ return {
45
+ default: 'preview',
46
+ authorization: '--apply --yes',
47
+ note: 'Supply required options shown by command --help. A preview does not apply changes.',
48
+ };
49
+ return { default: 'read-only', authorization: 'none' };
50
+ }