@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,111 @@
1
+ /** Pins one agent process to one provider account. The proxy routes a model
2
+ * named `<prefix>/<model>` only to the credential that owns the prefix, but
3
+ * agents send whichever model the user picked. This loopback relay lives only
4
+ * as long as the agent it was started for: it adds the prefix to each request
5
+ * and forwards everything else (credentials, streaming) unchanged.
6
+ */
7
+ export function startAccountRelay(
8
+ endpoint: string,
9
+ prefix: string
10
+ ): { url: string; stop: () => void } {
11
+ const server = Bun.serve({
12
+ hostname: '127.0.0.1',
13
+ port: 0,
14
+ // Model streams are idle for as long as a model thinks.
15
+ idleTimeout: 0,
16
+ fetch: (request) => forward(request, endpoint, prefix),
17
+ });
18
+ return {
19
+ url: `http://127.0.0.1:${server.port}`,
20
+ stop: () => void server.stop(true),
21
+ };
22
+ }
23
+
24
+ type RelayOptions = {
25
+ send?: typeof fetch;
26
+ pause?: (seconds: number) => Promise<void>;
27
+ };
28
+ const sleep = (seconds: number) =>
29
+ new Promise<void>((done) => setTimeout(done, seconds * 1000));
30
+
31
+ export async function forward(
32
+ request: Request,
33
+ endpoint: string,
34
+ prefix: string,
35
+ { send = fetch, pause = sleep }: RelayOptions = {}
36
+ ): Promise<Response> {
37
+ const url = new URL(request.url);
38
+ const headers = new Headers(request.headers);
39
+ // fetch sets these for the upstream request and decodes its response.
40
+ for (const name of ['host', 'content-length', 'accept-encoding'])
41
+ headers.delete(name);
42
+ let body: string | undefined;
43
+ if (request.body) {
44
+ // An unreadable body could not be pinned; never forward it to the pool.
45
+ if (headers.has('content-encoding'))
46
+ return new Response('The account relay needs an uncompressed request.', {
47
+ status: 415,
48
+ });
49
+ body = pinModel(await request.text(), prefix);
50
+ }
51
+ const attempt = () =>
52
+ send(endpoint + url.pathname + url.search, {
53
+ method: request.method,
54
+ headers,
55
+ ...(body === undefined ? {} : { body }),
56
+ redirect: 'error',
57
+ signal: request.signal,
58
+ });
59
+ const upstream = await reachProxy(attempt, request.signal, pause);
60
+ if (!upstream)
61
+ return new Response('The CLI proxy is unreachable.', { status: 502 });
62
+ const answered = new Headers(upstream.headers);
63
+ answered.delete('content-encoding');
64
+ answered.delete('content-length');
65
+ return new Response(upstream.body, {
66
+ status: upstream.status,
67
+ headers: answered,
68
+ });
69
+ }
70
+
71
+ /** Seconds to wait before each further attempt (about a minute in total). */
72
+ export const RETRY_DELAYS = [1, 2, 4, 8, 15, 30];
73
+ /** The route to the proxy answered instead of the proxy: its connector or the
74
+ * proxy itself is restarting. 502-504 from a gateway, 520-530 from Cloudflare.
75
+ */
76
+ const routeDown = (status: number) =>
77
+ (status >= 502 && status <= 504) || (status >= 520 && status <= 530);
78
+
79
+ /** A brief outage of the route must not end an agent's turn: nothing has been
80
+ * sent to the agent yet, so the same request is simply tried again until the
81
+ * proxy answers or the delays run out. The last answer is returned as it is.
82
+ */
83
+ async function reachProxy(
84
+ attempt: () => Promise<Response>,
85
+ signal: AbortSignal,
86
+ pause: (seconds: number) => Promise<void>
87
+ ): Promise<Response | null> {
88
+ for (let tries = 0; ; tries++) {
89
+ const delay = RETRY_DELAYS[tries];
90
+ const response = await attempt().catch(() => null);
91
+ if (response && !routeDown(response.status)) return response;
92
+ if (delay === undefined || signal.aborted) return response;
93
+ await response?.body?.cancel().catch(() => undefined);
94
+ await pause(delay);
95
+ }
96
+ }
97
+
98
+ /** The JSON request with its model routed to the account; other bodies as-is. */
99
+ export function pinModel(text: string, prefix: string): string {
100
+ try {
101
+ const payload: unknown = JSON.parse(text);
102
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
103
+ const model = (payload as { model?: unknown }).model;
104
+ if (typeof model === 'string' && !model.startsWith(`${prefix}/`))
105
+ return JSON.stringify({ ...payload, model: `${prefix}/${model}` });
106
+ }
107
+ } catch {
108
+ /* Not JSON: nothing to route. */
109
+ }
110
+ return text;
111
+ }
@@ -0,0 +1,85 @@
1
+ import { AgentConfigurationError } from './errors';
2
+ import { registry } from './registry';
3
+
4
+ /** One signed-in provider account on the proxy, offered through one tool. */
5
+ export type Account = {
6
+ tool: string;
7
+ email: string;
8
+ /** Models named `<prefix>/<model>` are served only by this account. */
9
+ prefix: string;
10
+ /** Every model the account serves, preferred default first. */
11
+ models: string[];
12
+ };
13
+
14
+ type Route = { provider: string; models: string[] };
15
+ const ROUTES = registry.proxy.accounts as Record<string, Route>;
16
+ const EMAIL = /^[^\s@/]{1,64}@[^\s@/]{1,255}$/;
17
+
18
+ /** Stable file and instance name for an account. */
19
+ export const accountSlug = (value: string): string =>
20
+ value.toLowerCase().replace(/[^a-z0-9]+/g, '_');
21
+
22
+ /** The proxy gives each account credential the prefix `<type>-<email>`. */
23
+ export function accountPrefix(tool: string, email: string): string {
24
+ const route = ROUTES[tool];
25
+ if (!route || !EMAIL.test(email))
26
+ throw new AgentConfigurationError(
27
+ 'Unknown provider account; T3 offers only accounts signed in on the CLI proxy.'
28
+ );
29
+ return `${route.provider}-${email}`;
30
+ }
31
+
32
+ /** Accounts are never declared in source: the proxy lists a prefixed copy of
33
+ * each model for every signed-in credential, and that list is the roster.
34
+ */
35
+ export function accountsFrom(modelIds: string[]): Account[] {
36
+ const accounts: Account[] = [];
37
+ for (const [tool, route] of Object.entries(ROUTES)) {
38
+ const served = new Map<string, string[]>();
39
+ for (const id of modelIds) {
40
+ const slash = id.indexOf('/');
41
+ const owner = id.slice(0, slash);
42
+ if (slash < 0 || !owner.startsWith(`${route.provider}-`)) continue;
43
+ const email = owner.slice(route.provider.length + 1);
44
+ if (!EMAIL.test(email)) continue;
45
+ served.set(email, [...(served.get(email) ?? []), id.slice(slash + 1)]);
46
+ }
47
+ for (const email of [...served.keys()].sort())
48
+ accounts.push({
49
+ tool,
50
+ email,
51
+ prefix: `${route.provider}-${email}`,
52
+ models: preferred(served.get(email)!.sort(), route.models),
53
+ });
54
+ }
55
+ return accounts;
56
+ }
57
+
58
+ const preferred = (models: string[], first: string[]) => [
59
+ ...first.filter((model) => models.includes(model)),
60
+ ...models.filter((model) => !first.includes(model)),
61
+ ];
62
+
63
+ /** Reads the roster with this node's own proxy key. Throws when the proxy does
64
+ * not answer: an unknown roster must never read as "no accounts".
65
+ */
66
+ export async function discoverAccounts(
67
+ key: string,
68
+ send: typeof fetch = fetch
69
+ ): Promise<Account[]> {
70
+ const response = await send(registry.proxy.endpoint + '/v1/models', {
71
+ headers: { Authorization: `Bearer ${key}` },
72
+ redirect: 'error',
73
+ signal: AbortSignal.timeout(15_000),
74
+ });
75
+ if (!response.ok)
76
+ throw new AgentConfigurationError('The CLI proxy did not list its models.');
77
+ const body = (await response.json()) as { data?: { id?: unknown }[] };
78
+ if (!Array.isArray(body.data))
79
+ throw new AgentConfigurationError('The CLI proxy did not list its models.');
80
+ return accountsFrom(
81
+ body.data.flatMap((model) =>
82
+ typeof model.id === 'string' ? [model.id] : []
83
+ )
84
+ );
85
+ }
@@ -0,0 +1,77 @@
1
+ import { join } from 'node:path';
2
+ import { homedir } from 'node:os';
3
+ import { readFile, readdir } from 'node:fs/promises';
4
+ import type { Platform } from '../platform/types';
5
+
6
+ /** Content values never leave this module; the audit inventories ownership boundaries. */
7
+ export async function auditMachine(platform: Platform) {
8
+ const home = homedir();
9
+ const paths = [
10
+ join(home, '.codex', 'config.toml'),
11
+ join(home, '.claude', 'settings.json'),
12
+ join(home, '.claude.json'),
13
+ join(home, '.gemini', 'settings.json'),
14
+ join(platform.opencodeDir(), 'opencode.json'),
15
+ join(home, '.cursor', 'mcp.json'),
16
+ join(home, '.t3', 'userdata', 'settings.json'),
17
+ join(home, '.config', 'cliproxy-fleet', 'settings.json'),
18
+ ];
19
+ const files = [];
20
+ for (const path of paths) files.push(await inspect(path));
21
+ const directories = [];
22
+ for (const tool of ['.codex', '.claude', '.gemini', '.cursor']) {
23
+ for (const kind of ['skills', 'plugins']) {
24
+ const path = join(home, tool, kind);
25
+ const entries = await readdir(path).catch(() => []);
26
+ directories.push({
27
+ tool,
28
+ kind,
29
+ entries: entries.length,
30
+ ownership: 'native/unmanaged; review sources before migration',
31
+ });
32
+ }
33
+ }
34
+ return {
35
+ version: 1,
36
+ files,
37
+ directories,
38
+ environment: Object.keys(process.env).filter((key) =>
39
+ /^(CODEX_|CLAUDE_|ANTHROPIC_|OPENAI_|GEMINI_|GOOGLE_|OPENCODE_|T3CODE_|CLIPROXY_|CURSOR_)/.test(
40
+ key
41
+ )
42
+ ),
43
+ warning:
44
+ 'Metadata only; values, credentials and runtime state are withheld. This machine is not proof of fleet-wide coverage. Native/project/organization settings may add configuration.',
45
+ };
46
+ }
47
+
48
+ async function inspect(path: string) {
49
+ try {
50
+ const text = await readFile(path, 'utf8');
51
+ const value: unknown = path.endsWith('.toml')
52
+ ? Bun.TOML.parse(text)
53
+ : JSON.parse(text);
54
+ return {
55
+ path,
56
+ present: true,
57
+ fields: fieldNames(value),
58
+ ownership:
59
+ 'native/unmanaged; dedicated dev-fleet profiles do not overwrite this file',
60
+ };
61
+ } catch (error) {
62
+ return {
63
+ path,
64
+ present: (error as NodeJS.ErrnoException).code !== 'ENOENT',
65
+ fields: [],
66
+ ownership:
67
+ 'missing or unparseable; inspect locally, no raw error retained',
68
+ };
69
+ }
70
+ }
71
+
72
+ export function fieldNames(value: unknown): string[] {
73
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return [];
74
+ return Object.keys(value)
75
+ .filter((key) => /^[a-zA-Z_$][a-zA-Z0-9_$-]{0,63}$/.test(key))
76
+ .sort();
77
+ }
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bun
2
+ /** Internal entry fleet-sync runs on each node for agent profile commands
3
+ * (`agents apply|status|verify|run`). People use `devfleet workstation agents`.
4
+ */
5
+ import { Command } from 'commander';
6
+ import { registerAgents } from './command';
7
+
8
+ const program = new Command('fleet-agents')
9
+ .option('--json', 'Machine-readable output')
10
+ .option('--yes', 'Apply without confirmation')
11
+ .option('--non-interactive', 'Fail instead of prompting');
12
+ registerAgents(program, () => program.opts());
13
+ await program.parseAsync(process.argv);
@@ -0,0 +1,204 @@
1
+ /* eslint-disable no-console -- this command's contract is JSON on stdout. */
2
+ import { auditMachine } from './audit';
3
+ import { AgentConfigurationError } from './errors';
4
+ import type { Command } from 'commander';
5
+ import { join } from 'node:path';
6
+ import { homedir } from 'node:os';
7
+ import { currentPlatform } from '../platform';
8
+ import { registry, select } from './registry';
9
+ import { agentRoot } from './render';
10
+ import { convergeAgents, plan, publicPlan } from './converge';
11
+ import { launchProfile } from './launch';
12
+ import { verifyProvider } from './verify';
13
+
14
+ /** The global flags of the CLI that hosts these commands. */
15
+ type GlobalOpts = { json?: boolean | undefined; yes?: boolean | undefined };
16
+
17
+ /** The monorepo checkout that contains this package. */
18
+ const workspaceRoot = (): string => join(import.meta.dir, '../..');
19
+
20
+ async function revision(requireClean: boolean): Promise<string> {
21
+ if (requireClean) await requireCleanSources();
22
+ const child = Bun.spawn(['git', 'rev-parse', 'HEAD'], {
23
+ cwd: workspaceRoot(),
24
+ stdout: 'pipe',
25
+ stderr: 'ignore',
26
+ });
27
+ const value = (await new Response(child.stdout).text()).trim();
28
+ if ((await child.exited) || !/^[a-f0-9]{40}$/.test(value))
29
+ throw new AgentConfigurationError('Cannot resolve checkout revision.');
30
+ return value;
31
+ }
32
+
33
+ type AgentOptions = {
34
+ node: string;
35
+ tool: string;
36
+ profile: string;
37
+ adopt?: boolean;
38
+ smoke?: boolean;
39
+ };
40
+
41
+ export function registerAgents(
42
+ program: Command,
43
+ globals: () => GlobalOpts
44
+ ): void {
45
+ const agents = program
46
+ .command('agents')
47
+ .description('[ws] Versioned agent/provider profiles');
48
+ agents
49
+ .command('inventory')
50
+ .action(() => console.log(JSON.stringify(registry, null, 2)));
51
+ agents
52
+ .command('audit')
53
+ .action(async () =>
54
+ console.log(
55
+ JSON.stringify(await auditMachine(currentPlatform()), null, 2)
56
+ )
57
+ );
58
+ registerT3Plan(agents);
59
+ for (const action of ['preview', 'apply', 'status', 'verify', 'run']) {
60
+ agents
61
+ .command(action)
62
+ .argument('[args...]')
63
+ .requiredOption('--node <id>', 'Explicit fleet node ID')
64
+ .option(
65
+ '--tool <tool>',
66
+ 'codex|claude|opencode|gemini|grok|antigravity',
67
+ 'codex'
68
+ )
69
+ .option('--profile <profile>', 'direct|proxy', 'direct')
70
+ .option(
71
+ '--adopt',
72
+ 'Back up and replace conflicting dedicated profile files'
73
+ )
74
+ .option(
75
+ '--smoke',
76
+ 'Spend tokens: one request, 16 output tokens, 30 second deadline'
77
+ )
78
+ .action(async (args: string[], options: AgentOptions) => {
79
+ try {
80
+ await execute(action, args, options, globals());
81
+ } catch (error) {
82
+ console.log(
83
+ JSON.stringify({
84
+ ok: false,
85
+ error:
86
+ error instanceof AgentConfigurationError
87
+ ? error.message
88
+ : 'Agent operation failed. Check node/tool/profile, review conflicts with preview, authenticate this node to Vault for proxy, and check approved model availability. No raw error or response is retained.',
89
+ })
90
+ );
91
+ process.exitCode = 1;
92
+ }
93
+ });
94
+ }
95
+ }
96
+
97
+ async function execute(
98
+ action: string,
99
+ args: string[],
100
+ options: AgentOptions,
101
+ global: GlobalOpts
102
+ ) {
103
+ const selection = select(options.node, options.tool, options.profile);
104
+ const root = agentRoot(homedir(), currentPlatform().name);
105
+ const context = {
106
+ root,
107
+ selection,
108
+ revision: await revision(['apply', 'run', 'verify'].includes(action)),
109
+ };
110
+ if (action === 'apply') {
111
+ if (!global.yes) throw new AgentConfigurationError('Apply requires --yes.');
112
+ console.log(
113
+ JSON.stringify({
114
+ ok: true,
115
+ ...(await convergeAgents(context, options.adopt)),
116
+ })
117
+ );
118
+ return;
119
+ }
120
+ const result = await plan(context);
121
+ const clean =
122
+ result.files.every((file) => file.state === 'unchanged') &&
123
+ result.previous?.fingerprint === result.fingerprint;
124
+ if (['run', 'verify'].includes(action)) {
125
+ if (!clean)
126
+ throw new AgentConfigurationError(
127
+ 'Apply the reviewed configuration first.'
128
+ );
129
+ await runOrVerify(action, root, selection, {
130
+ args,
131
+ smoke: options.smoke === true,
132
+ });
133
+ return;
134
+ }
135
+ console.log(
136
+ JSON.stringify({
137
+ ok: action === 'preview' || clean,
138
+ ...publicPlan(result, context.revision),
139
+ })
140
+ );
141
+ if (action === 'status' && !clean) process.exitCode = 1;
142
+ }
143
+
144
+ async function runOrVerify(
145
+ action: string,
146
+ root: string,
147
+ selection: ReturnType<typeof select>,
148
+ input: { args: string[]; smoke: boolean }
149
+ ) {
150
+ if (action === 'run') {
151
+ process.exitCode = await launchProfile(root, selection, input.args);
152
+ return;
153
+ }
154
+ console.log(
155
+ JSON.stringify({
156
+ ok: true,
157
+ ...(await verifyProvider(selection, input.smoke)),
158
+ })
159
+ );
160
+ }
161
+
162
+ function registerT3Plan(agents: Command): void {
163
+ agents
164
+ .command('t3-plan')
165
+ .requiredOption('--node <id>', 'Fleet node ID')
166
+ .action((options: { node: string }) => {
167
+ const selection = select(options.node, 'codex', 'proxy');
168
+ const root = agentRoot(homedir(), currentPlatform().name);
169
+ console.log(
170
+ JSON.stringify(
171
+ {
172
+ applied: false,
173
+ instance: 'personal_codex_proxy',
174
+ driver: 'codex',
175
+ config: {
176
+ binaryPath: join(workspaceRoot(), 'agents/codex-proxy.ts'),
177
+ homePath: join(root, selection.node, 'codex', 'proxy'),
178
+ },
179
+ environment: [{ name: 'WS_AGENT_NODE', value: selection.node }],
180
+ instruction:
181
+ 'Use these explicitly owned fields in a new T3 Codex provider instance. Preserve all other settings and secret references. Native Windows executable launch has not been validated.',
182
+ },
183
+ null,
184
+ 2
185
+ )
186
+ );
187
+ });
188
+ }
189
+
190
+ async function requireCleanSources(): Promise<void> {
191
+ const child = Bun.spawn(
192
+ ['git', 'status', '--porcelain', '--', 'agents', 'src/agents'],
193
+ {
194
+ cwd: workspaceRoot(),
195
+ stdout: 'pipe',
196
+ stderr: 'ignore',
197
+ }
198
+ );
199
+ const output = await new Response(child.stdout).text();
200
+ if ((await child.exited) || output.trim())
201
+ throw new AgentConfigurationError(
202
+ 'Fleet agent sources are dirty. Commit/review configuration before applying or launching a managed profile. Local work was preserved.'
203
+ );
204
+ }
@@ -0,0 +1,123 @@
1
+ import { AgentConfigurationError } from './errors';
2
+ import { mkdir, rm } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { digest, registry, type Selection } from './registry';
5
+ import { render } from './render';
6
+ import {
7
+ atomicWrite,
8
+ backup,
9
+ fileState,
10
+ readOwned,
11
+ safeParents,
12
+ } from './files';
13
+
14
+ export type Receipt = {
15
+ version: 1;
16
+ revision: string;
17
+ fingerprint: string;
18
+ files: Record<string, string>;
19
+ };
20
+ export type Context = { root: string; revision: string; selection: Selection };
21
+
22
+ export async function plan(context: Context) {
23
+ const { root, selection } = context;
24
+ const directory = join(
25
+ root,
26
+ selection.node,
27
+ selection.tool,
28
+ selection.profile
29
+ );
30
+ const receiptPath = join(directory, 'receipt.json');
31
+ const raw = await readOwned(receiptPath);
32
+ const previous = raw ? parseReceipt(raw) : null;
33
+ const outputs = render(selection);
34
+ const files = [];
35
+ for (const [name, content] of Object.entries(outputs)) {
36
+ const path = join(directory, name);
37
+ const current = await readOwned(path);
38
+ files.push({
39
+ name,
40
+ path,
41
+ content,
42
+ state: fileState(current, content, previous?.files[name]),
43
+ });
44
+ }
45
+ const fingerprint = digest(JSON.stringify({ registry, selection, outputs }));
46
+ return { directory, receiptPath, previous, files, fingerprint };
47
+ }
48
+
49
+ export async function convergeAgents(context: Context, adopt = false) {
50
+ await safeParents(context.root);
51
+ await mkdir(context.root, { recursive: true, mode: 0o700 });
52
+ const lock = join(context.root, '.apply-lock');
53
+ try {
54
+ await mkdir(lock);
55
+ } catch {
56
+ throw new AgentConfigurationError(
57
+ 'Another apply is active; inspect .apply-lock before retrying.'
58
+ );
59
+ }
60
+ try {
61
+ const result = await plan(context);
62
+ if (!adopt && result.files.some((file) => file.state === 'conflict'))
63
+ throw new AgentConfigurationError(
64
+ 'Unmanaged or modified configuration. Review locally, then use --adopt to back up and replace only this profile.'
65
+ );
66
+ for (const file of result.files) {
67
+ if (file.state === 'unchanged') continue;
68
+ if ((await readOwned(file.path)) !== null)
69
+ await backup(file.path, context.root);
70
+ await atomicWrite(file.path, file.content);
71
+ }
72
+ const receipt: Receipt = {
73
+ version: 1,
74
+ revision: context.revision,
75
+ fingerprint: result.fingerprint,
76
+ files: Object.fromEntries(
77
+ result.files.map((file) => [file.name, digest(file.content)])
78
+ ),
79
+ };
80
+ const encoded = JSON.stringify(receipt, null, 2) + '\n';
81
+ if ((await readOwned(result.receiptPath)) !== encoded)
82
+ await atomicWrite(result.receiptPath, encoded);
83
+ return {
84
+ ...publicPlan(result, context.revision),
85
+ appliedRevision: context.revision,
86
+ receiptMatches: true,
87
+ };
88
+ } finally {
89
+ await rm(lock, { recursive: true, force: true });
90
+ }
91
+ }
92
+
93
+ export function publicPlan(
94
+ result: Awaited<ReturnType<typeof plan>>,
95
+ revision: string
96
+ ) {
97
+ return {
98
+ revision,
99
+ appliedRevision: result.previous?.revision ?? null,
100
+ fingerprint: result.fingerprint,
101
+ receiptMatches: result.previous?.fingerprint === result.fingerprint,
102
+ files: result.files.map(({ name, state }) => ({ name, state })),
103
+ t3EndToEnd: 'unverified',
104
+ authenticatedInference: 'unverified',
105
+ };
106
+ }
107
+
108
+ function parseReceipt(raw: string): Receipt {
109
+ const value = JSON.parse(raw) as Receipt;
110
+ if (
111
+ value.version !== 1 ||
112
+ !/^[a-f0-9]{40}$/.test(value.revision) ||
113
+ !/^[a-f0-9]{64}$/.test(value.fingerprint) ||
114
+ !value.files ||
115
+ !Object.values(value.files).every(
116
+ (hash) => typeof hash === 'string' && /^[a-f0-9]{64}$/.test(hash)
117
+ )
118
+ )
119
+ throw new AgentConfigurationError(
120
+ 'Invalid configuration receipt. Inspect it locally and restore a known receipt before applying.'
121
+ );
122
+ return value;
123
+ }