@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,226 @@
1
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import type { Check, Runner } from '../types';
5
+ import { pinnedPath } from './toolchain';
6
+ import { registry } from './registry';
7
+ import {
8
+ cacheKey,
9
+ cachedCheck,
10
+ sourceKey,
11
+ type VerifyCache,
12
+ } from './verify-cache';
13
+
14
+ const PROMPT = 'Reply with exactly: OK';
15
+
16
+ /** Every agent T3 offers on every node, launched exactly as T3 launches it:
17
+ * through the tracked wrapper that injects the shared credential. `pin` names
18
+ * the mise.toml tool whose locked version the installed binary must report.
19
+ */
20
+ export const NATIVE_AGENTS = [
21
+ {
22
+ name: 'claude',
23
+ binary: 'claude',
24
+ pin: 'claude',
25
+ wrapper: 'claude-proxy.ts',
26
+ args: ['-p', PROMPT],
27
+ },
28
+ {
29
+ name: 'codex',
30
+ binary: 'codex',
31
+ pin: 'codex',
32
+ wrapper: 'codex-proxy.ts',
33
+ args: ['exec', '--skip-git-repo-check', PROMPT],
34
+ },
35
+ {
36
+ name: 'grok',
37
+ binary: 'grok',
38
+ pin: 'http:grok',
39
+ wrapper: 'grok-proxy.ts',
40
+ args: ['-p', PROMPT],
41
+ },
42
+ {
43
+ name: 'antigravity',
44
+ binary: 'antigravity',
45
+ pin: 'http:antigravity',
46
+ wrapper: 'antigravity-proxy.ts',
47
+ args: [
48
+ '-p',
49
+ PROMPT,
50
+ '--model',
51
+ 'Gemini 3.8 Flash (High)',
52
+ '--print-timeout',
53
+ '90s',
54
+ ],
55
+ },
56
+ {
57
+ name: 'muse',
58
+ binary: 'opencode',
59
+ pin: 'aqua:anomalyco/opencode',
60
+ wrapper: 'opencode-proxy.ts',
61
+ args: ['run', '--model', 'personal_proxy/devin/muse-wonder-1-0', PROMPT],
62
+ },
63
+ ] as const;
64
+
65
+ type Agent = (typeof NATIVE_AGENTS)[number];
66
+
67
+ /** Same versions everywhere (from the shared mise.lock) and a working signed-in
68
+ * session through each T3 launcher. Agents run concurrently and are bounded.
69
+ */
70
+ export async function verifyNativeAgents(
71
+ node: string,
72
+ workspace: string,
73
+ run: Runner,
74
+ options: {
75
+ /** Proxy routes already found out of quota; their sessions are not run. */
76
+ outOfQuota?: Set<string>;
77
+ cache?: VerifyCache;
78
+ } = {}
79
+ ): Promise<Check[]> {
80
+ const { outOfQuota = new Set<string>(), cache = {} } = options;
81
+ const pins = parsePins(await readFile(join(workspace, 'mise.toml'), 'utf8'));
82
+ const cwd = await mkdtemp(join(tmpdir(), 'fleet-agents-'));
83
+ try {
84
+ const results = await Promise.all(
85
+ NATIVE_AGENTS.map(async (agent) => [
86
+ await versionCheck(agent, pins[agent.pin], run),
87
+ outOfQuota.has(agent.name)
88
+ ? quotaPass(agent)
89
+ : await cachedCheck(
90
+ `agent-${node}-${agent.name}`,
91
+ cacheKey(
92
+ node,
93
+ agent,
94
+ pins[agent.pin] ?? '',
95
+ registry,
96
+ await sourceKey(launcherSources(workspace, agent))
97
+ ),
98
+ () => sessionCheck(agent, { node, workspace, cwd }, run),
99
+ cache
100
+ ),
101
+ ])
102
+ );
103
+ return results.flat();
104
+ } finally {
105
+ await rm(cwd, { recursive: true, force: true });
106
+ }
107
+ }
108
+
109
+ /** Reads `tool = "x"` and `"tool" = { version = "x", ... }` pins. */
110
+ export function parsePins(source: string): Record<string, string> {
111
+ const pins: Record<string, string> = {};
112
+ const pattern =
113
+ /^"?([A-Za-z0-9:@/._-]+)"?\s*=\s*(?:"([^"]+)"|\{[^\n]*?\bversion\s*=\s*"([^"]+)")/gm;
114
+ for (const match of source.matchAll(pattern))
115
+ pins[match[1]!] = (match[2] ?? match[3])!;
116
+ return pins;
117
+ }
118
+
119
+ /** Self-healing: a binary that drifted from the lock (a vendor self-updater
120
+ * rewrote it) is reinstalled from the locked artifact, then checked again.
121
+ */
122
+ async function versionCheck(
123
+ agent: Agent,
124
+ pin: string | undefined,
125
+ run: Runner
126
+ ): Promise<Check> {
127
+ const matches = async () => {
128
+ const result = await run([agent.binary, '--version'], {
129
+ env: { PATH: pinnedPath() },
130
+ timeoutMs: 30_000,
131
+ });
132
+ return Boolean(pin) && result.ok && result.stdout.includes(pin!);
133
+ };
134
+ let ok = await matches();
135
+ if (!ok && pin) {
136
+ await run(
137
+ ['mise', 'install', '--locked', '--force', `${agent.pin}@${pin}`],
138
+ {
139
+ timeoutMs: 600_000,
140
+ }
141
+ );
142
+ ok = await matches();
143
+ }
144
+ return {
145
+ id: `agent:${agent.name}:version`,
146
+ status: ok ? 'pass' : 'fail',
147
+ summary: ok
148
+ ? `${agent.binary} ${pin} matches the shared toolchain lock.`
149
+ : 'AGENT_VERSION_MISMATCH',
150
+ ...(ok
151
+ ? {}
152
+ : {
153
+ remedy: {
154
+ action: `Every node runs the ${agent.binary} pinned in mise.toml/mise.lock; reinstalling it from the lock failed. Inspect mise install --locked ${agent.pin} on this node; never install it with a vendor self-updater.`,
155
+ },
156
+ }),
157
+ };
158
+ }
159
+
160
+ /** Quota is the account's, not the node's: no warning (as for providers). */
161
+ function quotaPass(agent: Agent): Check {
162
+ return {
163
+ id: `agent:${agent.name}:session`,
164
+ status: 'pass',
165
+ summary: `The shared ${agent.name} account is rate limited or out of quota; its session is checked again once it recovers.`,
166
+ };
167
+ }
168
+
169
+ async function sessionCheck(
170
+ agent: Agent,
171
+ context: { node: string; workspace: string; cwd: string },
172
+ run: Runner
173
+ ): Promise<Check> {
174
+ const launch = () =>
175
+ run(
176
+ ['bun', join(context.workspace, 'agents', agent.wrapper), ...agent.args],
177
+ {
178
+ cwd: context.cwd,
179
+ env: { WS_AGENT_NODE: context.node, PATH: pinnedPath() },
180
+ input: '',
181
+ timeoutMs: 150_000,
182
+ }
183
+ );
184
+ const expected = /\bOK\b/;
185
+ let result = await launch();
186
+ // One retry: a CLI's first launch on a node may still be initializing.
187
+ if (!(result.ok && expected.test(result.stdout))) result = await launch();
188
+ const ok = result.ok && expected.test(result.stdout);
189
+ return {
190
+ id: `agent:${agent.name}:session`,
191
+ status: ok ? 'pass' : 'fail',
192
+ summary: ok
193
+ ? `${agent.name} runs signed in through its T3 launcher with the shared account.`
194
+ : sessionFailure(result.stderr + result.stdout),
195
+ ...(ok ? {} : { remedy: { action: sessionRemedy(agent) } }),
196
+ };
197
+ }
198
+
199
+ /** A stable code only; raw CLI output is never reported. */
200
+ function sessionFailure(output: string): string {
201
+ if (/missing or out of date/.test(output)) return 'AGENT_PROFILE_STALE';
202
+ if (/Required secret unavailable/.test(output))
203
+ return 'AGENT_VAULT_UNAVAILABLE';
204
+ if (/model_not_found|unknown provider for model/.test(output))
205
+ return 'AGENT_MODEL_UNSERVED';
206
+ return 'AGENT_SESSION_UNVERIFIED';
207
+ }
208
+
209
+ function sessionRemedy(agent: Agent): string {
210
+ if (agent.name === 'antigravity')
211
+ return 'Antigravity sends its own catalog model ids; the proxy serves them through declared aliases that fleet-sync applies on the manager (see proxy:config-pending). Pick Gemini 3.8 Flash in T3: the default Pro model is not served by the shared accounts.';
212
+ return `Run the ${agent.name} launcher on this node to inspect it: WS_AGENT_NODE=<node> bun agents/${agent.wrapper}. Check this node's Vault session and the shared proxy.`;
213
+ }
214
+
215
+ /** The launcher and profile code a session depends on. */
216
+ function launcherSources(workspace: string, agent: Agent): string[] {
217
+ const agents = join(workspace, 'agents');
218
+ const lib = join(workspace, 'src/agents');
219
+ return [
220
+ join(agents, agent.wrapper),
221
+ join(agents, 'managed-proxy.ts'),
222
+ ...['launch.ts', 'render.ts', 'secrets.ts', 'toolchain.ts'].map((name) =>
223
+ join(lib, name)
224
+ ),
225
+ ];
226
+ }
@@ -0,0 +1,50 @@
1
+ import { AgentConfigurationError } from './errors';
2
+
3
+ export class ProviderVerificationError extends AgentConfigurationError {
4
+ constructor(
5
+ readonly code: string,
6
+ message: string
7
+ ) {
8
+ super(message);
9
+ }
10
+ }
11
+ export function providerHttpFailure(status: number): ProviderVerificationError {
12
+ const code =
13
+ status === 401 || status === 403
14
+ ? 'PROVIDER_AUTH_REJECTED'
15
+ : status === 429
16
+ ? 'PROVIDER_RATE_LIMITED'
17
+ : status >= 500
18
+ ? 'PROVIDER_UPSTREAM_UNAVAILABLE'
19
+ : 'PROVIDER_PROTOCOL_REJECTED';
20
+ return new ProviderVerificationError(
21
+ code,
22
+ 'The gateway rejected a bounded verification request; response content is withheld.'
23
+ );
24
+ }
25
+ const SAFE_CODES = new Set([
26
+ 'PROVIDER_AUTH_REJECTED',
27
+ 'PROVIDER_RATE_LIMITED',
28
+ 'PROVIDER_UPSTREAM_UNAVAILABLE',
29
+ 'PROVIDER_PROTOCOL_REJECTED',
30
+ 'PROVIDER_MODEL_UNAVAILABLE',
31
+ 'PROVIDER_OUTPUT_UNVERIFIED',
32
+ 'PROVIDER_TOOL_RESULT_UNVERIFIED',
33
+ 'PROVIDER_EMPTY_RESPONSE',
34
+ 'PROVIDER_RESPONSE_LIMIT',
35
+ 'PROVIDER_TOOL_MISSING',
36
+ 'PROVIDER_TOOL_INVALID',
37
+ 'PROVIDER_TOOL_ARGUMENTS_INVALID',
38
+ 'PROVIDER_STREAM_INCOMPLETE',
39
+ ]);
40
+ export function providerFailureCode(error: unknown): string {
41
+ if (error instanceof ProviderVerificationError && SAFE_CODES.has(error.code))
42
+ return error.code;
43
+ if (error instanceof Error && SAFE_CODES.has(error.message))
44
+ return error.message;
45
+ if (error instanceof Error && error.name === 'TimeoutError')
46
+ return 'PROVIDER_TIMEOUT';
47
+ if (error instanceof TypeError) return 'PROVIDER_NETWORK_UNVERIFIED';
48
+ if (error instanceof SyntaxError) return 'PROVIDER_RESPONSE_INVALID';
49
+ return 'PROVIDER_ROUTE_UNVERIFIED';
50
+ }
@@ -0,0 +1,55 @@
1
+ import { AgentConfigurationError } from './errors';
2
+ import source from '../../agents/registry.json';
3
+ import { createHash } from 'node:crypto';
4
+ import { fleet } from '../fleet-config';
5
+
6
+ export type Tool = keyof typeof source.defaults;
7
+ export type Profile = 'direct' | 'proxy';
8
+ export type Selection = {
9
+ node: string;
10
+ tool: Tool;
11
+ profile: Profile;
12
+ model: string;
13
+ };
14
+ /** The tracked registry plus the proxy endpoint from this fleet's config. */
15
+ export const registry = {
16
+ ...source,
17
+ proxy: { endpoint: fleet.services.proxy.endpoint, ...source.proxy },
18
+ };
19
+
20
+ export function select(node: string, tool: string, profile: string): Selection {
21
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(node))
22
+ throw new AgentConfigurationError('Invalid node ID.');
23
+ if (!(tool in source.defaults) || !['direct', 'proxy'].includes(profile))
24
+ throw new AgentConfigurationError(
25
+ 'Unsupported tool/profile; run bun src/agents/cli.ts agents inventory.'
26
+ );
27
+ const id = tool as Tool;
28
+ const model = resolveModel(node, id, profile as Profile);
29
+ if (!model || !/^[a-zA-Z0-9_./:+-]+$/.test(model))
30
+ throw new AgentConfigurationError(
31
+ 'No approved model. Add an exact model ID to agents/registry.json and review it.'
32
+ );
33
+ return { node, tool: id, profile: profile as Profile, model };
34
+ }
35
+
36
+ export function resolveModel(
37
+ node: string,
38
+ tool: Tool,
39
+ profile: Profile,
40
+ nodes = source.nodes as Record<
41
+ string,
42
+ Partial<Record<Tool, { direct?: string; proxy?: string }>>
43
+ >
44
+ ) {
45
+ const override = nodes[node]?.[tool]?.[profile];
46
+ const models = source.proxy.models as Partial<Record<Tool, string | null>>;
47
+ return (
48
+ override ??
49
+ (profile === 'direct' ? source.defaults[tool].model : models[tool])
50
+ );
51
+ }
52
+
53
+ export function digest(value: string): string {
54
+ return createHash('sha256').update(value).digest('hex');
55
+ }
@@ -0,0 +1,110 @@
1
+ import { join, win32 } from 'node:path';
2
+ import type { PlatformName } from '../platform/types';
3
+ import { registry, type Selection } from './registry';
4
+
5
+ /** Dedicated outputs; native login/state directories are never copied. */
6
+ export function agentRoot(home: string, platform: PlatformName): string {
7
+ return platform === 'windows'
8
+ ? win32.join(home, 'AppData', 'Local', 'personal', 'agents')
9
+ : join(home, '.config', 'personal', 'agents');
10
+ }
11
+
12
+ export function render(s: Selection): Record<string, string> {
13
+ const model = JSON.stringify(s.model);
14
+ if (s.tool === 'codex') {
15
+ const provider =
16
+ s.profile === 'proxy'
17
+ ? '\nmodel_provider = "personal_proxy"\n[model_providers.personal_proxy]\nname = "Personal CLI Proxy"\nbase_url = ' +
18
+ JSON.stringify(registry.proxy.endpoint + '/v1') +
19
+ '\nenv_key = "CLIPROXY_API_KEY"\nwire_api = "responses"\nrequires_openai_auth = false\nsupports_websockets = false\n'
20
+ : '\n';
21
+ return { 'config.toml': `model = ${model}${provider}` };
22
+ }
23
+ if (s.tool === 'claude')
24
+ return {
25
+ 'settings.json': JSON.stringify({ model: s.model }, null, 2) + '\n',
26
+ };
27
+ if (s.tool === 'grok') return { 'config.toml': grokConfig(s) };
28
+ if (s.tool === 'antigravity')
29
+ return {
30
+ // The fleet profile home for direct and one-shot runs (see launchArgs);
31
+ // never the human's ~/.gemini. T3 keeps its own private GEMINI_HOME.
32
+ '.gemini/antigravity-cli/settings.json':
33
+ JSON.stringify({ modelProvider: 'gemini' }, null, 2) + '\n',
34
+ };
35
+ if (s.tool === 'gemini')
36
+ return {
37
+ '.gemini/settings.json':
38
+ JSON.stringify(
39
+ {
40
+ model: { name: s.model },
41
+ ...(s.profile === 'proxy'
42
+ ? { security: { auth: { selectedType: 'gateway' } } }
43
+ : {}),
44
+ },
45
+ null,
46
+ 2
47
+ ) + '\n',
48
+ };
49
+ const config =
50
+ s.profile === 'proxy'
51
+ ? {
52
+ $schema: 'https://opencode.ai/config.json',
53
+ model: `personal_proxy/${s.model}`,
54
+ provider: {
55
+ personal_proxy: {
56
+ npm: '@ai-sdk/openai',
57
+ options: {
58
+ baseURL: registry.proxy.endpoint + '/v1',
59
+ apiKey: '{env:CLIPROXY_API_KEY}',
60
+ },
61
+ models: Object.fromEntries(
62
+ [
63
+ ...Object.values(registry.proxy.routes).map(
64
+ (route) => route.model
65
+ ),
66
+ s.model,
67
+ ].map((id) => [id, { name: id }])
68
+ ),
69
+ },
70
+ },
71
+ }
72
+ : { $schema: 'https://opencode.ai/config.json', model: s.model };
73
+ return { 'opencode.json': JSON.stringify(config, null, 2) + '\n' };
74
+ }
75
+
76
+ /** Grok Build reads custom models only from GROK_HOME/config.toml. The key
77
+ * comes from the environment (env_key), never the file; no grok login.
78
+ */
79
+ function grokConfig(s: Selection): string {
80
+ // Grok records a one-time skills migration in config.toml; declaring it up
81
+ // front keeps the rendered file unchanged by the CLI (no profile drift).
82
+ const lines = [
83
+ '[cli]',
84
+ 'auto_update = false',
85
+ '',
86
+ '[marketplace]',
87
+ 'default_skills_installs_purged = true',
88
+ '',
89
+ ];
90
+ if (s.profile !== 'proxy')
91
+ return [
92
+ ...lines,
93
+ '[models]',
94
+ `default = ${JSON.stringify(s.model)}`,
95
+ '',
96
+ ].join('\n');
97
+ return [
98
+ ...lines,
99
+ '[models]',
100
+ 'default = "personal-proxy"',
101
+ '',
102
+ '[model.personal-proxy]',
103
+ `model = ${JSON.stringify(s.model)}`,
104
+ `name = ${JSON.stringify(`${s.model} (Personal CLI Proxy)`)}`,
105
+ `base_url = ${JSON.stringify(registry.proxy.endpoint + '/v1')}`,
106
+ 'env_key = "CLIPROXY_API_KEY"',
107
+ 'api_backend = "chat_completions"',
108
+ '',
109
+ ].join('\n');
110
+ }
@@ -0,0 +1,192 @@
1
+ import { providerHttpFailure } from './provider-errors';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { registry } from './registry';
4
+ import { record } from '../config';
5
+
6
+ /** Reasoning models spend output tokens thinking before any text or tool call,
7
+ * so a tight cap ends the response without the tool call or text being
8
+ * verified. Prompts are tiny; this is a ceiling, not an expected length.
9
+ */
10
+ export const VERIFY_OUTPUT_TOKENS = 1024;
11
+
12
+ /** Supported OpenAI Responses harness route. The only tool is a local echo;
13
+ * model output can never select a command, file, URL or other side effect.
14
+ */
15
+ export async function verifyRoundTrip(
16
+ model: string,
17
+ key: string,
18
+ request = fetch
19
+ ): Promise<void> {
20
+ await verifyStream(model, key, request);
21
+ const nonce = randomUUID();
22
+ const tool = {
23
+ type: 'function',
24
+ name: 'fleet_echo',
25
+ description: 'Return the provided verification nonce.',
26
+ parameters: {
27
+ type: 'object',
28
+ properties: { nonce: { type: 'string', enum: [nonce] } },
29
+ required: ['nonce'],
30
+ additionalProperties: false,
31
+ },
32
+ strict: true,
33
+ };
34
+ const prompt = {
35
+ role: 'user',
36
+ content: `Call fleet_echo with nonce ${nonce}, then reply with its result.`,
37
+ };
38
+ const response = await call(
39
+ model,
40
+ key,
41
+ {
42
+ input: [prompt],
43
+ tools: [tool],
44
+ tool_choice: { type: 'function', name: 'fleet_echo' },
45
+ max_output_tokens: VERIFY_OUTPUT_TOKENS,
46
+ },
47
+ request
48
+ );
49
+ const payload: unknown = JSON.parse(await boundedText(response));
50
+ const invocation = validateInvocation(payload, nonce);
51
+ const final = await call(
52
+ model,
53
+ key,
54
+ {
55
+ input: [
56
+ prompt,
57
+ invocation,
58
+ {
59
+ type: 'function_call_output',
60
+ call_id: invocation.call_id,
61
+ output: nonce,
62
+ },
63
+ ],
64
+ tools: [tool],
65
+ tool_choice: 'none',
66
+ max_output_tokens: VERIFY_OUTPUT_TOKENS,
67
+ },
68
+ request
69
+ );
70
+ const completed: unknown = JSON.parse(await boundedText(final));
71
+ if (
72
+ !record(completed) ||
73
+ completed['status'] !== 'completed' ||
74
+ !Array.isArray(completed['output']) ||
75
+ !JSON.stringify(completed['output']).includes(nonce)
76
+ )
77
+ throw new Error('PROVIDER_TOOL_RESULT_UNVERIFIED');
78
+ }
79
+ async function call(
80
+ model: string,
81
+ key: string,
82
+ body: object,
83
+ request: typeof fetch
84
+ ): Promise<Response> {
85
+ const response = await request(registry.proxy.endpoint + '/v1/responses', {
86
+ method: 'POST',
87
+ headers: {
88
+ Authorization: `Bearer ${key}`,
89
+ 'Content-Type': 'application/json',
90
+ },
91
+ body: JSON.stringify({ model, ...body }),
92
+ redirect: 'error',
93
+ signal: AbortSignal.timeout(45_000),
94
+ });
95
+ if (!response.ok) throw providerHttpFailure(response.status);
96
+ return response;
97
+ }
98
+ async function boundedText(response: Response): Promise<string> {
99
+ if (!response.body) throw new Error('PROVIDER_EMPTY_RESPONSE');
100
+ const reader = response.body.getReader();
101
+ const decoder = new TextDecoder();
102
+ let size = 0;
103
+ let text = '';
104
+ try {
105
+ while (true) {
106
+ const chunk = await reader.read();
107
+ if (chunk.done) break;
108
+ size += chunk.value.byteLength;
109
+ if (size > 512 * 1024) throw new Error('PROVIDER_RESPONSE_LIMIT');
110
+ text += decoder.decode(chunk.value, { stream: true });
111
+ }
112
+ return text + decoder.decode();
113
+ } finally {
114
+ await reader.cancel();
115
+ }
116
+ }
117
+ function validateInvocation(
118
+ payload: unknown,
119
+ nonce: string
120
+ ): {
121
+ type: 'function_call';
122
+ call_id: string;
123
+ name: 'fleet_echo';
124
+ arguments: string;
125
+ } {
126
+ if (!record(payload)) throw new Error('PROVIDER_TOOL_MISSING');
127
+ if (!Array.isArray(payload['output']))
128
+ throw new Error('PROVIDER_TOOL_MISSING');
129
+ const calls = payload['output'].filter(
130
+ (item) => record(item) && item['type'] === 'function_call'
131
+ );
132
+ const call = calls[0];
133
+ if (
134
+ calls.length !== 1 ||
135
+ !record(call) ||
136
+ call['name'] !== 'fleet_echo' ||
137
+ !stringFields(call, ['call_id', 'arguments'])
138
+ )
139
+ throw new Error('PROVIDER_TOOL_INVALID');
140
+ const args: unknown = JSON.parse(String(call['arguments']));
141
+ if (
142
+ !record(args) ||
143
+ Object.keys(args).length !== 1 ||
144
+ args['nonce'] !== nonce
145
+ )
146
+ throw new Error('PROVIDER_TOOL_ARGUMENTS_INVALID');
147
+ return {
148
+ type: 'function_call',
149
+ name: 'fleet_echo',
150
+ call_id: String(call['call_id']),
151
+ arguments: String(call['arguments']),
152
+ };
153
+ }
154
+
155
+ async function verifyStream(
156
+ model: string,
157
+ key: string,
158
+ request: typeof fetch
159
+ ): Promise<void> {
160
+ const response = await call(
161
+ model,
162
+ key,
163
+ {
164
+ input: 'Reply OK.',
165
+ stream: true,
166
+ max_output_tokens: VERIFY_OUTPUT_TOKENS,
167
+ },
168
+ request
169
+ );
170
+ const body = await boundedText(response);
171
+ const events: unknown[] = body
172
+ .split(/\r?\n/)
173
+ .filter((line) => line.startsWith('data:'))
174
+ .map((line) => line.slice(5).trim())
175
+ .filter((line) => line !== '[DONE]')
176
+ .map((line) => JSON.parse(line));
177
+ const types = events.filter(record).map((event) => event['type']);
178
+ if (
179
+ !types.includes('response.completed') ||
180
+ !types.includes('response.output_text.delta') ||
181
+ types.includes('response.failed') ||
182
+ types.includes('error')
183
+ )
184
+ throw new Error('PROVIDER_STREAM_INCOMPLETE');
185
+ }
186
+
187
+ function stringFields(
188
+ value: Record<string, unknown>,
189
+ fields: string[]
190
+ ): boolean {
191
+ return fields.every((field) => typeof value[field] === 'string');
192
+ }
@@ -0,0 +1,13 @@
1
+ import { AgentConfigurationError } from './errors';
2
+ import { secret } from '../fleet-config';
3
+
4
+ /** The key every node presents to the CLI proxy, from this fleet's secrets
5
+ * provider with this node's own access.
6
+ */
7
+ export async function proxyKey(): Promise<string> {
8
+ const value = await secret('proxy-client-key');
9
+ if (value) return value;
10
+ throw new AgentConfigurationError(
11
+ "Required secret unavailable: proxy-client-key. Restore this node's access to the fleet's secrets provider (see `secrets` in the fleet configuration)."
12
+ );
13
+ }
@@ -0,0 +1,34 @@
1
+ import { homedir } from 'node:os';
2
+ import { delimiter, join } from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+
5
+ /** mise shims resolve every agent to the version pinned in the shared lock.
6
+ * Putting them first keeps an unmanaged copy elsewhere on PATH (a vendor
7
+ * installer in ~/.local/bin, an nvm global) from shadowing the pinned one.
8
+ */
9
+ export function pinnedPath(env: NodeJS.ProcessEnv = process.env): string {
10
+ const data =
11
+ env['MISE_DATA_DIR'] ??
12
+ join(env['XDG_DATA_HOME'] ?? join(homedir(), '.local', 'share'), 'mise');
13
+ const shims = join(data, 'shims');
14
+ const rest = (env['PATH'] ?? '')
15
+ .split(delimiter)
16
+ .filter((entry) => entry && entry !== shims);
17
+ return [shims, ...rest].join(delimiter);
18
+ }
19
+
20
+ /** The real pinned executable behind a mise shim. Needed when a launch
21
+ * changes HOME: a shim run under another HOME cannot find mise's trust state.
22
+ */
23
+ export function pinnedBinary(
24
+ tool: string,
25
+ env: NodeJS.ProcessEnv = process.env
26
+ ): string {
27
+ const result = spawnSync('mise', ['which', tool], {
28
+ encoding: 'utf8',
29
+ env: { ...env, PATH: pinnedPath(env) },
30
+ timeout: 15_000,
31
+ });
32
+ const path = result.status === 0 ? result.stdout.trim() : '';
33
+ return path.startsWith('/') ? path : tool;
34
+ }