@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,107 @@
1
+ import { lstat, readdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { agentRoot } from './render';
5
+ import { registry, select } from './registry';
6
+ import { plan } from './converge';
7
+ import type { Platform } from '../platform/types';
8
+ import type { DoctorCheck } from '../doctor-check';
9
+
10
+ export async function agentChecks(platform: Platform): Promise<DoctorCheck[]> {
11
+ const checks: DoctorCheck[] = [];
12
+ for (const [tool, definition] of Object.entries(registry.tools)) {
13
+ const binary = Bun.which(definition.binary);
14
+ checks.push({
15
+ id: `agent:${tool}`,
16
+ label: `${tool} installed`,
17
+ severity: binary ? 'pass' : 'warn',
18
+ detail: binary
19
+ ? await version(definition.binary)
20
+ : 'Not on PATH. Use shared mise for managed CLIs; desktop apps remain OS prerequisites.',
21
+ });
22
+ }
23
+ const root = agentRoot(homedir(), platform.name);
24
+ const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
25
+ let profiles = 0;
26
+ for (const node of entries
27
+ .filter(
28
+ (entry) =>
29
+ entry.isDirectory() && /^[a-z0-9][a-z0-9-]{0,62}$/.test(entry.name)
30
+ )
31
+ .slice(0, 64)) {
32
+ for (const tool of Object.keys(registry.defaults)) {
33
+ for (const profile of ['direct', 'proxy']) {
34
+ const check = await profileCheck(root, node.name, tool, profile);
35
+ if (check) {
36
+ checks.push(check);
37
+ profiles++;
38
+ }
39
+ }
40
+ }
41
+ }
42
+ if (profiles === 0)
43
+ checks.push({
44
+ id: 'agent:profiles',
45
+ label: 'Managed profiles',
46
+ severity: 'warn',
47
+ detail:
48
+ 'No applied profiles. Run bun src/agents/cli.ts agents preview --node ID, then bun src/agents/cli.ts --yes agents apply --node ID.',
49
+ });
50
+ return checks;
51
+ }
52
+
53
+ async function profileCheck(
54
+ root: string,
55
+ node: string,
56
+ tool: string,
57
+ profile: string
58
+ ): Promise<DoctorCheck | null> {
59
+ const receiptPath = join(root, node, tool, profile, 'receipt.json');
60
+ if (!(await lstat(receiptPath).catch(() => null))) return null;
61
+ try {
62
+ const result = await plan({
63
+ root,
64
+ selection: select(node, tool, profile),
65
+ revision: 'unverified',
66
+ });
67
+ if (!result.previous) return null;
68
+ const clean =
69
+ result.files.every((file) => file.state === 'unchanged') &&
70
+ result.previous.fingerprint === result.fingerprint;
71
+ return {
72
+ id: `agent:${node}:${tool}:${profile}`,
73
+ label: 'Managed profile drift',
74
+ severity: clean ? 'pass' : 'fail',
75
+ detail: `Applied revision ${result.previous.revision}; static configuration ${clean ? 'matches' : 'drifted'}. Secret availability, endpoint, inference and T3 end-to-end require bun src/agents/cli.ts agents verify/operator tests.`,
76
+ };
77
+ } catch {
78
+ return {
79
+ id: `agent:${node}:${tool}:${profile}`,
80
+ label: 'Managed profile drift',
81
+ severity: 'fail',
82
+ detail:
83
+ 'Profile or receipt cannot be validated. Inspect locally and preview before applying; raw contents withheld.',
84
+ };
85
+ }
86
+ }
87
+
88
+ async function version(binary: string): Promise<string> {
89
+ try {
90
+ const child = Bun.spawn([binary, '--version'], {
91
+ stdout: 'pipe',
92
+ stderr: 'ignore',
93
+ });
94
+ const timer = setTimeout(() => child.kill(), 5000);
95
+ try {
96
+ const text = await new Response(child.stdout).text();
97
+ const match = text.match(/\b\d+\.\d+\.\d+(?:[-+][a-zA-Z0-9.-]+)?\b/);
98
+ return (await child.exited) === 0 && match
99
+ ? `Version ${match[0]}; installation pins remain owned by mise.`
100
+ : 'Version unverified.';
101
+ } finally {
102
+ clearTimeout(timer);
103
+ }
104
+ } catch {
105
+ return 'Version unverified.';
106
+ }
107
+ }
@@ -0,0 +1,2 @@
1
+ /** Only explicitly authored messages may cross the agent CLI error boundary. */
2
+ export class AgentConfigurationError extends Error {}
@@ -0,0 +1,73 @@
1
+ import { AgentConfigurationError } from './errors';
2
+ import {
3
+ lstat,
4
+ mkdir,
5
+ readFile,
6
+ rename,
7
+ rm,
8
+ writeFile,
9
+ copyFile,
10
+ chmod,
11
+ } from 'node:fs/promises';
12
+ import { dirname, join, parse } from 'node:path';
13
+ import { randomUUID } from 'node:crypto';
14
+ import { digest } from './registry';
15
+
16
+ export async function safeParents(path: string): Promise<void> {
17
+ let current = path;
18
+ while (current !== parse(current).root) {
19
+ try {
20
+ if ((await lstat(current)).isSymbolicLink())
21
+ throw new AgentConfigurationError(
22
+ 'Symlink conflict; move the managed destination before applying.'
23
+ );
24
+ } catch (error) {
25
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
26
+ }
27
+ current = dirname(current);
28
+ }
29
+ }
30
+
31
+ export async function readOwned(path: string): Promise<string | null> {
32
+ await safeParents(path);
33
+ try {
34
+ if (!(await lstat(path)).isFile())
35
+ throw new AgentConfigurationError(
36
+ 'Managed destination is not a regular file.'
37
+ );
38
+ return await readFile(path, 'utf8');
39
+ } catch (error) {
40
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
41
+ throw error;
42
+ }
43
+ }
44
+
45
+ export async function atomicWrite(path: string, value: string): Promise<void> {
46
+ await safeParents(path);
47
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
48
+ const temp = `${path}.${randomUUID()}.tmp`;
49
+ try {
50
+ await writeFile(temp, value, { flag: 'wx', mode: 0o600 });
51
+ await rename(temp, path);
52
+ } finally {
53
+ await rm(temp, { force: true });
54
+ }
55
+ }
56
+
57
+ export async function backup(path: string, root: string): Promise<void> {
58
+ const dir = join(root, 'backups');
59
+ await mkdir(dir, { recursive: true, mode: 0o700 });
60
+ const target = join(dir, randomUUID());
61
+ await copyFile(path, target);
62
+ await chmod(target, 0o600);
63
+ }
64
+
65
+ export function fileState(
66
+ current: string | null,
67
+ desired: string,
68
+ previous?: string
69
+ ): string {
70
+ if (current === desired) return 'unchanged';
71
+ if (current === null) return 'create';
72
+ return previous === digest(current) ? 'update' : 'conflict';
73
+ }
@@ -0,0 +1,156 @@
1
+ import { providerFailureCode } from './provider-errors';
2
+ import { verifyRoundTrip } from './round-trip';
3
+ import { proxyKey } from './secrets';
4
+ import type { Check } from '../types';
5
+ import { registry, type Selection } from './registry';
6
+ import { verifyProvider } from './verify';
7
+ import { fileURLToPath } from 'node:url';
8
+ import {
9
+ cacheKey,
10
+ cachedCheck,
11
+ sourceKey,
12
+ type VerifyCache,
13
+ } from './verify-cache';
14
+
15
+ /** The verifier's own code: changing how providers are checked re-verifies.
16
+ * Resolved from the module URL: the published CLI loads this on Node, which has
17
+ * no `import.meta.dir`.
18
+ */
19
+ const VERIFIER_SOURCES = ['fleet-verify.ts', 'verify.ts', 'round-trip.ts'].map(
20
+ (name) => fileURLToPath(new URL(name, import.meta.url))
21
+ );
22
+
23
+ /** Uses the selected gateway credential in this node's noninteractive environment.
24
+ * Authentication/inference evidence does not certify native clients or paired T3.
25
+ */
26
+ export async function verifyFleetProviders(
27
+ node: string,
28
+ cache: VerifyCache = {}
29
+ ): Promise<Check[]> {
30
+ let key: string;
31
+ try {
32
+ key = await proxyKey();
33
+ } catch {
34
+ return Object.keys(registry.proxy.routes).map((provider) => ({
35
+ id: `provider:${provider}:authentication`,
36
+ status: 'fail',
37
+ summary: 'PROVIDER_UNATTENDED_CREDENTIAL_REQUIRED',
38
+ remedy: {
39
+ action:
40
+ 'Restore this node’s existing Vault authorization and renewal. No model requests or native OAuth login were attempted.',
41
+ },
42
+ }));
43
+ }
44
+
45
+ // Warm runs reuse recent passes, but always prove this node's key still
46
+ // authenticates to the live gateway; if it does not, verify everything.
47
+ const live = await gatewayAuthenticates(key);
48
+ const code = await sourceKey(VERIFIER_SOURCES);
49
+ // Providers are independent upstream accounts: verify them concurrently so
50
+ // the slowest one, not their sum, bounds the step. Order stays stable.
51
+ return Promise.all(
52
+ Object.entries(registry.proxy.routes).map(([provider, route]) =>
53
+ cachedCheck(
54
+ `provider-${node}-${provider}`,
55
+ cacheKey(node, registry.proxy.endpoint, route, code),
56
+ () => verifyRoute(node, provider, route, key),
57
+ {
58
+ ...cache,
59
+ bypass: Boolean(cache.bypass) || !live,
60
+ reuse: (check) => check.summary !== QUOTA_SUMMARY,
61
+ }
62
+ )
63
+ )
64
+ );
65
+ }
66
+
67
+ /** One cheap authenticated model listing: Vault key, proxy and route are live. */
68
+ async function gatewayAuthenticates(key: string): Promise<boolean> {
69
+ try {
70
+ const response = await fetch(registry.proxy.endpoint + '/v1/models', {
71
+ headers: { Authorization: `Bearer ${key}` },
72
+ redirect: 'error',
73
+ signal: AbortSignal.timeout(15_000),
74
+ });
75
+ return response.ok;
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+
81
+ async function verifyRoute(
82
+ node: string,
83
+ provider: string,
84
+ route: { model: string; native: string | null; harness: string },
85
+ key: string
86
+ ): Promise<Check> {
87
+ const selection: Selection = {
88
+ node,
89
+ profile: 'proxy',
90
+ tool: (route.native ?? route.harness) as Selection['tool'],
91
+ model: route.model,
92
+ };
93
+ try {
94
+ const evidence = await verifyProvider(selection, true, key);
95
+ await verifyRoundTrip(route.model, key);
96
+ return {
97
+ id: `provider:${provider}:inference`,
98
+ status: evidence.inference === 'pass' ? 'pass' : 'fail',
99
+ summary:
100
+ 'Gateway authentication, bounded inference, completed streaming and safe echo tool round trip verified from node environment. Native CLI/T3 execution is separate.',
101
+ };
102
+ } catch (error) {
103
+ return inferenceFailure(provider, providerFailureCode(error));
104
+ }
105
+ }
106
+
107
+ /** Rate limits and exhausted quota are expected on shared subscription
108
+ * accounts: the gateway authenticated and routed the request, and the account
109
+ * recovers on its own. They are not reported at all (a quiet pass).
110
+ */
111
+ const QUOTA = new Set(['PROVIDER_RATE_LIMITED']);
112
+ /** Summary of the quiet pass; native session checks skip these accounts. */
113
+ export const QUOTA_SUMMARY =
114
+ 'Gateway authenticated and routed the request; the provider account is rate limited or out of quota and recovers on its own.';
115
+
116
+ /** Proxy routes whose provider account is out of quota in these checks. */
117
+ export function quotaLimited(checks: Check[]): Set<string> {
118
+ return new Set(
119
+ checks
120
+ .filter((check) => check.summary === QUOTA_SUMMARY)
121
+ .map((check) => check.id.split(':')[1]!)
122
+ );
123
+ }
124
+
125
+ /** Other conditions owned by the upstream provider account (outage, billing)
126
+ * do not mean this node is misconfigured: the gateway answered and routed the
127
+ * request. They are warnings, so a provider outage never fails fleet-sync.
128
+ */
129
+ const UPSTREAM: Record<string, string> = {
130
+ PROVIDER_UPSTREAM_UNAVAILABLE:
131
+ 'The provider returned a server error through the gateway. Retry later; no fleet change is needed.',
132
+ PROVIDER_MODEL_UNAVAILABLE:
133
+ 'The provider refused the declared model (often billing or account access). Fix the provider account, or change the model in agents/registry.json.',
134
+ PROVIDER_TIMEOUT:
135
+ 'The provider did not answer within the bounded verification deadline. Retry later.',
136
+ };
137
+
138
+ export function inferenceFailure(provider: string, code: string): Check {
139
+ if (QUOTA.has(code))
140
+ return {
141
+ id: `provider:${provider}:inference`,
142
+ status: 'pass',
143
+ summary: QUOTA_SUMMARY,
144
+ };
145
+ const upstream = UPSTREAM[code];
146
+ return {
147
+ id: `provider:${provider}:inference`,
148
+ status: upstream ? 'warn' : 'fail',
149
+ summary: code,
150
+ remedy: {
151
+ action:
152
+ upstream ??
153
+ `Verify unattended Vault access and the declared ${provider} model on the singleton gateway. Preserve OAuth state; do not log native auth on proxy clients.`,
154
+ },
155
+ };
156
+ }
@@ -0,0 +1,290 @@
1
+ import { join } from 'node:path';
2
+ import { mkdirSync } from 'node:fs';
3
+ import { AgentConfigurationError } from './errors';
4
+ import { registry, type Selection } from './registry';
5
+ import { accountPrefix, accountSlug, discoverAccounts } from './accounts';
6
+ import { startAccountRelay } from './account-relay';
7
+ import { proxyKey } from './secrets';
8
+ import { pinnedBinary, pinnedPath } from './toolchain';
9
+
10
+ /** `account` (its email) pins a proxy launch to one signed-in proxy account. */
11
+ export async function launchProfile(
12
+ root: string,
13
+ selection: Selection,
14
+ args: string[],
15
+ account?: string
16
+ ): Promise<number> {
17
+ const directory = join(
18
+ root,
19
+ selection.node,
20
+ selection.tool,
21
+ selection.profile
22
+ );
23
+ const env = { ...process.env };
24
+ env['PATH'] = pinnedPath(env);
25
+ // Do not allow an ambient gateway to silently change the selected route.
26
+ for (const name of [
27
+ 'OPENAI_BASE_URL',
28
+ 'ANTHROPIC_BASE_URL',
29
+ 'ANTHROPIC_AUTH_TOKEN',
30
+ 'CLAUDE_CODE_USE_BEDROCK',
31
+ 'CLAUDE_CODE_USE_VERTEX',
32
+ 'CLAUDE_CODE_USE_FOUNDRY',
33
+ 'CLIPROXY_API_KEY',
34
+ 'GOOGLE_GEMINI_BASE_URL',
35
+ 'GROK_MODELS_BASE_URL',
36
+ 'GROK_CODE_XAI_API_KEY',
37
+ 'XAI_API_KEY',
38
+ ])
39
+ delete env[name];
40
+ if (selection.profile === 'proxy') {
41
+ env['CLIPROXY_API_KEY'] = await proxyKey();
42
+ configureGatewayEnvironment(env, selection);
43
+ }
44
+ toolEnvironment(env, selection, directory);
45
+ const pinned =
46
+ account && selection.profile === 'proxy'
47
+ ? await pinAccount(env, selection, { account, directory })
48
+ : null;
49
+ try {
50
+ return await spawnAgent(selection, directory, env, [
51
+ pinned?.args ?? [],
52
+ args,
53
+ ]);
54
+ } finally {
55
+ pinned?.stop();
56
+ }
57
+ }
58
+
59
+ async function spawnAgent(
60
+ selection: Selection,
61
+ directory: string,
62
+ env: NodeJS.ProcessEnv,
63
+ [pinned, args]: [string[], string[]]
64
+ ): Promise<number> {
65
+ // Resolve the executable before launchArgs may point HOME at the profile.
66
+ const executable =
67
+ selection.tool === 'antigravity' && args[0] !== 'acp'
68
+ ? pinnedBinary(selection.tool, env)
69
+ : selection.tool;
70
+ const command = [
71
+ executable,
72
+ ...pinned,
73
+ ...launchArgs(selection, directory, args, env),
74
+ ];
75
+ if (selection.tool === 'antigravity' && args[0] === 'acp')
76
+ return relayAcp(command, env);
77
+ const child = Bun.spawn(command, {
78
+ env,
79
+ stdin: 'inherit',
80
+ stdout: 'inherit',
81
+ stderr: 'inherit',
82
+ });
83
+ return child.exited;
84
+ }
85
+
86
+ /** Routes every request of this launch to one account's proxy credential.
87
+ * Claude and Codex send whichever model was picked, so they talk to a loopback
88
+ * relay that adds the account's model prefix. OpenCode lists exactly the
89
+ * account's prefixed models, so it needs no relay.
90
+ */
91
+ export async function pinAccount(
92
+ env: NodeJS.ProcessEnv,
93
+ selection: Selection,
94
+ { account, directory }: { account: string; directory: string },
95
+ { relay = startAccountRelay, discover = discoverAccounts } = {}
96
+ ): Promise<{ args: string[]; stop: () => void }> {
97
+ const prefix = accountPrefix(selection.tool, account);
98
+ // What the account serves right now, preferred default first.
99
+ const served = (await discover(env['CLIPROXY_API_KEY'] ?? '')).find(
100
+ (entry) => entry.tool === selection.tool && entry.email === account
101
+ );
102
+ if (!served)
103
+ throw new AgentConfigurationError(
104
+ 'This account is not signed in on the CLI proxy.'
105
+ );
106
+ if (selection.tool === 'opencode') {
107
+ delete env['OPENCODE_CONFIG'];
108
+ env['OPENCODE_CONFIG_CONTENT'] = JSON.stringify(accountOpenCode(served));
109
+ return { args: [], stop: () => {} };
110
+ }
111
+ const { url, stop } = relay(registry.proxy.endpoint, prefix);
112
+ if (selection.tool === 'claude') {
113
+ env['ANTHROPIC_BASE_URL'] = url;
114
+ // Aliases and the default must name models a Claude account serves.
115
+ const [opus, sonnet, haiku] = (
116
+ registry.proxy.accounts as Record<string, { models: string[] }>
117
+ )['claude']!.models;
118
+ env['ANTHROPIC_DEFAULT_OPUS_MODEL'] = opus;
119
+ env['ANTHROPIC_DEFAULT_SONNET_MODEL'] = sonnet;
120
+ env['ANTHROPIC_DEFAULT_HAIKU_MODEL'] = haiku;
121
+ env['ANTHROPIC_MODEL'] = sonnet;
122
+ return { args: [], stop };
123
+ }
124
+ // Each account keeps its own Codex sessions; the provider is declared on
125
+ // the command line because that home has no rendered config.
126
+ env['CODEX_HOME'] = join(directory, '..', `account-${accountSlug(account)}`);
127
+ mkdirSync(env['CODEX_HOME'], { recursive: true, mode: 0o700 });
128
+ const provider = {
129
+ name: 'Personal CLI Proxy',
130
+ base_url: `${url}/v1`,
131
+ env_key: 'CLIPROXY_API_KEY',
132
+ wire_api: 'responses',
133
+ requires_openai_auth: false,
134
+ supports_websockets: false,
135
+ };
136
+ return {
137
+ args: [
138
+ ['model', served.models[0]],
139
+ ['model_provider', 'personal_proxy'],
140
+ ...Object.entries(provider).map(
141
+ ([key, value]) =>
142
+ [`model_providers.personal_proxy.${key}`, value] as const
143
+ ),
144
+ ].flatMap(([key, value]) => ['-c', `${key}=${JSON.stringify(value)}`]),
145
+ stop,
146
+ };
147
+ }
148
+
149
+ /** OpenCode configuration that offers only one account's models. */
150
+ function accountOpenCode(route: { prefix: string; models: string[] }) {
151
+ const models = route.models.map((model) => `${route.prefix}/${model}`);
152
+ return {
153
+ $schema: 'https://opencode.ai/config.json',
154
+ model: `personal_proxy/${models[0]}`,
155
+ enabled_providers: ['personal_proxy'],
156
+ provider: {
157
+ personal_proxy: {
158
+ npm: '@ai-sdk/openai',
159
+ options: {
160
+ baseURL: registry.proxy.endpoint + '/v1',
161
+ apiKey: '{env:CLIPROXY_API_KEY}',
162
+ },
163
+ models: Object.fromEntries(
164
+ models.map((id) => [id, { name: id.slice(route.prefix.length + 1) }])
165
+ ),
166
+ },
167
+ },
168
+ };
169
+ }
170
+
171
+ /** Per-tool profile selection and self-updater switches. */
172
+ function toolEnvironment(
173
+ env: NodeJS.ProcessEnv,
174
+ selection: Selection,
175
+ directory: string
176
+ ): void {
177
+ if (selection.tool === 'claude') env['DISABLE_AUTOUPDATER'] = '1';
178
+ if (selection.tool === 'codex') env['CODEX_HOME'] = directory;
179
+ if (selection.tool === 'opencode')
180
+ env['OPENCODE_CONFIG'] = join(directory, 'opencode.json');
181
+ if (selection.tool === 'gemini') env['GEMINI_CLI_HOME'] = directory;
182
+ if (selection.tool === 'grok') {
183
+ env['GROK_HOME'] = directory;
184
+ env['GROK_DISABLE_AUTOUPDATER'] = '1';
185
+ }
186
+ // Only "true" disables Antigravity's in-place self-updater ("1" is ignored).
187
+ if (selection.tool === 'antigravity')
188
+ env['AGY_CLI_DISABLE_AUTO_UPDATE'] = 'true';
189
+ }
190
+
191
+ /** T3's managed policy may not carry a credential, so its Antigravity
192
+ * instance keeps the default Google sign-in method. The proxy key is the
193
+ * credential here: the one ACP `authenticate` request is switched to the
194
+ * Gemini API-key method on its way in; every other message passes unchanged.
195
+ */
196
+ async function relayAcp(
197
+ command: string[],
198
+ env: NodeJS.ProcessEnv
199
+ ): Promise<number> {
200
+ const child = Bun.spawn(command, {
201
+ env,
202
+ stdin: 'pipe',
203
+ stdout: 'inherit',
204
+ stderr: 'inherit',
205
+ });
206
+ void (async () => {
207
+ let pending = '';
208
+ for await (const chunk of process.stdin) {
209
+ pending += String(chunk);
210
+ let end = pending.indexOf('\n');
211
+ while (end >= 0) {
212
+ void child.stdin.write(
213
+ rewriteAuthenticate(pending.slice(0, end)) + '\n'
214
+ );
215
+ pending = pending.slice(end + 1);
216
+ end = pending.indexOf('\n');
217
+ }
218
+ void child.stdin.flush();
219
+ }
220
+ if (pending) void child.stdin.write(rewriteAuthenticate(pending));
221
+ void child.stdin.end();
222
+ })();
223
+ return child.exited;
224
+ }
225
+
226
+ const BROWSER_SIGN_IN = new Set(['oauth-personal', 'oauth-business']);
227
+
228
+ /** Rewrites only an ACP `authenticate` request for a browser sign-in method. */
229
+ export function rewriteAuthenticate(line: string): string {
230
+ if (!line.includes('"authenticate"')) return line;
231
+ try {
232
+ const message = JSON.parse(line) as {
233
+ method?: unknown;
234
+ params?: { methodId?: unknown };
235
+ };
236
+ if (
237
+ message.method !== 'authenticate' ||
238
+ !BROWSER_SIGN_IN.has(String(message.params?.methodId))
239
+ )
240
+ return line;
241
+ return JSON.stringify({
242
+ ...message,
243
+ params: { ...message.params, methodId: 'gemini-api-key' },
244
+ });
245
+ } catch {
246
+ return line;
247
+ }
248
+ }
249
+
250
+ /** Tool-specific arguments that select the rendered profile. */
251
+ export function launchArgs(
252
+ selection: Selection,
253
+ directory: string,
254
+ args: string[],
255
+ env: NodeJS.ProcessEnv
256
+ ): string[] {
257
+ if (selection.tool === 'claude')
258
+ return ['--settings', join(directory, 'settings.json'), ...args];
259
+ if (selection.tool === 'antigravity') {
260
+ // Over ACP (how T3 runs it) T3 owns a private GEMINI_HOME and selects the
261
+ // Gemini API key method; the proxy key and endpoint come from this wrapper.
262
+ // Run directly, the fleet profile is the home; one-shot print mode only
263
+ // reads ~/.gemini, so it also runs with HOME set to the profile.
264
+ if (args[0] === 'acp') {
265
+ env['GEMINI_HOME'] ??= join(directory, '.gemini');
266
+ return ['acp', '--disable-telemetry', ...args.slice(1)];
267
+ }
268
+ env['HOME'] = directory;
269
+ }
270
+ return args;
271
+ }
272
+
273
+ function configureGatewayEnvironment(
274
+ env: NodeJS.ProcessEnv,
275
+ selection: Selection
276
+ ): void {
277
+ if (selection.tool === 'gemini' || selection.tool === 'antigravity') {
278
+ delete env['GOOGLE_GENAI_USE_VERTEXAI'];
279
+ delete env['GOOGLE_GENAI_USE_GCA'];
280
+ env['GOOGLE_GEMINI_BASE_URL'] = registry.proxy.endpoint;
281
+ env['GEMINI_API_KEY'] = env['CLIPROXY_API_KEY'];
282
+ }
283
+ if (selection.tool === 'claude') {
284
+ delete env['ANTHROPIC_API_KEY'];
285
+ env['ANTHROPIC_BASE_URL'] = registry.proxy.endpoint;
286
+ env['ANTHROPIC_AUTH_TOKEN'] = env['CLIPROXY_API_KEY'];
287
+ for (const family of ['OPUS', 'SONNET', 'HAIKU'])
288
+ env[`ANTHROPIC_DEFAULT_${family}_MODEL`] = selection.model;
289
+ }
290
+ }