@crvouga/dev-fleet 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (363) hide show
  1. package/.mise/locks/aws-cdk/2.1146.1/aube-lock.yaml +24 -0
  2. package/.mise/locks/aws-cdk/2.1146.1/package.json +7 -0
  3. package/.mise/locks/azure-cli/2.91.0~a837e014/pyproject.toml +5 -0
  4. package/.mise/locks/azure-cli/2.91.0~a837e014/uv.lock +2794 -0
  5. package/.mise/locks/gemini/0.66.0~84aac8e6/aube-lock.yaml +123 -0
  6. package/.mise/locks/gemini/0.66.0~84aac8e6/package.json +12 -0
  7. package/.mise/locks/npm-devcontainers-cli/0.90.0/aube-lock.yaml +24 -0
  8. package/.mise/locks/npm-devcontainers-cli/0.90.0/package.json +7 -0
  9. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/aube-lock.yaml +1432 -0
  10. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/package.json +7 -0
  11. package/.mise/locks/npm-vercel/65.8.0/aube-lock.yaml +3595 -0
  12. package/.mise/locks/npm-vercel/65.8.0/package.json +7 -0
  13. package/.mise/locks/npm-wrangler/4.156.0/aube-lock.yaml +895 -0
  14. package/.mise/locks/npm-wrangler/4.156.0/package.json +7 -0
  15. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/aube-lock.yaml +24 -0
  16. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/package.json +7 -0
  17. package/LICENSE +21 -0
  18. package/README.md +357 -2
  19. package/agents/antigravity-proxy.ts +3 -0
  20. package/agents/audit.md +89 -0
  21. package/agents/claude-proxy +5 -0
  22. package/agents/claude-proxy.ts +3 -0
  23. package/agents/codex-proxy.ts +3 -0
  24. package/agents/grok-proxy.ts +3 -0
  25. package/agents/managed-proxy.ts +55 -0
  26. package/agents/opencode-proxy.ts +3 -0
  27. package/agents/registry.json +209 -0
  28. package/agents/registry.schema.json +149 -0
  29. package/agents/setup.md +158 -0
  30. package/agents/t3.json +40 -0
  31. package/ansible/macos/manager.yml +30 -0
  32. package/ansible/macos/policy.json +9 -0
  33. package/ansible/monitoring/README.md +215 -0
  34. package/ansible/monitoring/agent-darwin.yml +198 -0
  35. package/ansible/monitoring/agent.yml +172 -0
  36. package/ansible/monitoring/beszel-agent.service +42 -0
  37. package/ansible/monitoring/compose.yml +74 -0
  38. package/ansible/monitoring/hub-darwin.yml +102 -0
  39. package/ansible/monitoring/hub-route-darwin.yml +51 -0
  40. package/ansible/monitoring/lock.json +16 -0
  41. package/ansible/net/remote-probe.sh +13 -0
  42. package/ansible/pyproject.toml +12 -0
  43. package/ansible/ubuntu/authorize-sudo.sh +51 -0
  44. package/ansible/ubuntu/baseline.yml +89 -0
  45. package/ansible/ubuntu/disk-gc.py +336 -0
  46. package/ansible/ubuntu/disk-guard.py +104 -0
  47. package/ansible/ubuntu/policy.json +52 -0
  48. package/ansible/ubuntu/recovery.yml +15 -0
  49. package/ansible/ubuntu/t3/policy.json +7 -0
  50. package/ansible/ubuntu/t3-desktop.yml +220 -0
  51. package/ansible/ubuntu/t3-health.sh +33 -0
  52. package/ansible/ubuntu/t3-policy.yml +76 -0
  53. package/ansible/ubuntu/t3-route.yml +28 -0
  54. package/ansible/ubuntu/tailscale.yml +78 -0
  55. package/ansible/ubuntu/tasks/credentials.yml +47 -0
  56. package/ansible/ubuntu/tasks/docker.yml +31 -0
  57. package/ansible/ubuntu/tasks/packages.yml +56 -0
  58. package/ansible/ubuntu/tasks/power.yml +51 -0
  59. package/ansible/ubuntu/tasks/recovery.yml +39 -0
  60. package/ansible/ubuntu/tasks/resources.yml +312 -0
  61. package/ansible/ubuntu/tasks/services.yml +31 -0
  62. package/ansible/ubuntu/tasks/ssh.yml +71 -0
  63. package/ansible/ubuntu/tasks/user.yml +120 -0
  64. package/ansible/uv.lock +190 -0
  65. package/bin/devfleet.mjs +36 -0
  66. package/bootstrap/bootstrap.ps1 +50 -0
  67. package/bootstrap/bootstrap.sh +80 -0
  68. package/bootstrap/devfleet +7 -0
  69. package/bootstrap/docker-desktop.sh +33 -0
  70. package/bootstrap/doctor.ts +234 -0
  71. package/bootstrap/fleet-manager-run.sh +103 -0
  72. package/bootstrap/fleet-remote-bootstrap.sh +258 -0
  73. package/bootstrap/global-config.ts +171 -0
  74. package/bootstrap/install-launcher.sh +10 -0
  75. package/bootstrap/native-aws.ts +86 -0
  76. package/bootstrap/native-t3.ts +154 -0
  77. package/bootstrap/npm.ts +358 -0
  78. package/bootstrap/release-version.ts +157 -0
  79. package/bootstrap/setup-status.ts +51 -0
  80. package/bootstrap/setup.ts +88 -0
  81. package/bootstrap/shell-path.ts +105 -0
  82. package/bootstrap/verify-t3-archive.py +49 -0
  83. package/bootstrap/worker-auth.ts +65 -0
  84. package/bootstrap/worker-github.ts +47 -0
  85. package/bootstrap/worker-tools.ts +24 -0
  86. package/cliproxy/AGENTS.md +11 -0
  87. package/cliproxy/README.md +269 -0
  88. package/cliproxy/clients/bootstrap.py +143 -0
  89. package/cliproxy/clients/launcher.py +57 -0
  90. package/cliproxy/compose.yaml +90 -0
  91. package/cliproxy/config.template.yaml +70 -0
  92. package/cliproxy/docs/cloud-adoption.md +16 -0
  93. package/cliproxy/docs/deployment.png +0 -0
  94. package/cliproxy/docs/deployment.svg +109 -0
  95. package/cliproxy/docs/health-workflow.example.yml +185 -0
  96. package/cliproxy/docs/migration.md +131 -0
  97. package/cliproxy/docs/model-health-report.schema.json +364 -0
  98. package/cliproxy/docs/model-health.md +393 -0
  99. package/cliproxy/image.lock.json +5 -0
  100. package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
  101. package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
  102. package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
  103. package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
  104. package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
  105. package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
  106. package/cliproxy/models.json +1618 -0
  107. package/cliproxy/requirements.lock +78 -0
  108. package/cliproxy/scripts/accounts.py +38 -0
  109. package/cliproxy/scripts/audit.py +123 -0
  110. package/cliproxy/scripts/backup +234 -0
  111. package/cliproxy/scripts/check.py +34 -0
  112. package/cliproxy/scripts/checkpoint.py +226 -0
  113. package/cliproxy/scripts/cli.py +64 -0
  114. package/cliproxy/scripts/cliproxy_host.py +257 -0
  115. package/cliproxy/scripts/cloud_state.py +150 -0
  116. package/cliproxy/scripts/container-health +6 -0
  117. package/cliproxy/scripts/docker +24 -0
  118. package/cliproxy/scripts/external-health.py +79 -0
  119. package/cliproxy/scripts/fleet_config.py +43 -0
  120. package/cliproxy/scripts/fleet_sync.py +274 -0
  121. package/cliproxy/scripts/initialize +84 -0
  122. package/cliproxy/scripts/install-dependencies +48 -0
  123. package/cliproxy/scripts/install.py +406 -0
  124. package/cliproxy/scripts/model_health.py +16 -0
  125. package/cliproxy/scripts/modelhealth/__init__.py +5 -0
  126. package/cliproxy/scripts/modelhealth/checks.py +429 -0
  127. package/cliproxy/scripts/modelhealth/cli.py +117 -0
  128. package/cliproxy/scripts/modelhealth/config.py +186 -0
  129. package/cliproxy/scripts/modelhealth/emulator.py +536 -0
  130. package/cliproxy/scripts/modelhealth/expect.py +88 -0
  131. package/cliproxy/scripts/modelhealth/findings.py +183 -0
  132. package/cliproxy/scripts/modelhealth/protocol.py +330 -0
  133. package/cliproxy/scripts/modelhealth/redact.py +28 -0
  134. package/cliproxy/scripts/modelhealth/report.py +150 -0
  135. package/cliproxy/scripts/modelhealth/runner.py +390 -0
  136. package/cliproxy/scripts/modelhealth/scenario.py +131 -0
  137. package/cliproxy/scripts/modelhealth/schema.py +63 -0
  138. package/cliproxy/scripts/modelhealth/simulate.py +111 -0
  139. package/cliproxy/scripts/modelhealth/transport.py +213 -0
  140. package/cliproxy/scripts/monitor +68 -0
  141. package/cliproxy/scripts/ops +527 -0
  142. package/cliproxy/scripts/public-endpoint +127 -0
  143. package/cliproxy/scripts/recover_node.py +177 -0
  144. package/cliproxy/scripts/restore-verify +60 -0
  145. package/cliproxy/scripts/retention.py +144 -0
  146. package/cliproxy/scripts/service-start +7 -0
  147. package/cliproxy/scripts/validate-config +56 -0
  148. package/cliproxy/scripts/vault-access.py +57 -0
  149. package/cliproxy/scripts/vault-backup-login +55 -0
  150. package/cliproxy/scripts/vault-recovery +56 -0
  151. package/cliproxy/scripts/vault-renew +30 -0
  152. package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
  153. package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
  154. package/cliproxy/systemd/cliproxy-backup.service +13 -0
  155. package/cliproxy/systemd/cliproxy-backup.timer +8 -0
  156. package/cliproxy/systemd/cliproxy-fleet.service +17 -0
  157. package/cliproxy/systemd/cliproxy-gc.service +13 -0
  158. package/cliproxy/systemd/cliproxy-gc.timer +8 -0
  159. package/cliproxy/systemd/cliproxy-monitor.service +8 -0
  160. package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
  161. package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
  162. package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
  163. package/cliproxy/vendor/LICENSE +22 -0
  164. package/cliproxy/vendor/config.upstream.yaml +1349 -0
  165. package/docs/consumer-setup.md +408 -0
  166. package/docs/declared.md +89 -0
  167. package/docs/machine-setup.md +399 -0
  168. package/docs/operations.md +276 -0
  169. package/docs/releasing.md +103 -0
  170. package/docs/state.md +254 -0
  171. package/docs/topology.md +67 -0
  172. package/docs/ubuntu-sync.md +351 -0
  173. package/docs/worker-contract.md +67 -0
  174. package/examples/fleet.json +100 -0
  175. package/examples/roster.json +13 -0
  176. package/llms.txt +35 -0
  177. package/mise.lock +1521 -0
  178. package/mise.toml +110 -0
  179. package/package.json +74 -4
  180. package/schema/fleet.schema.json +420 -0
  181. package/schema/roster.schema.json +77 -0
  182. package/src/agents/account-relay.ts +111 -0
  183. package/src/agents/accounts.ts +85 -0
  184. package/src/agents/audit.ts +77 -0
  185. package/src/agents/cli.ts +13 -0
  186. package/src/agents/command.ts +204 -0
  187. package/src/agents/converge.ts +123 -0
  188. package/src/agents/doctor.ts +107 -0
  189. package/src/agents/errors.ts +2 -0
  190. package/src/agents/files.ts +73 -0
  191. package/src/agents/fleet-verify.ts +165 -0
  192. package/src/agents/launch.ts +290 -0
  193. package/src/agents/native-verify.ts +371 -0
  194. package/src/agents/provider-errors.ts +57 -0
  195. package/src/agents/registry.ts +61 -0
  196. package/src/agents/render.ts +110 -0
  197. package/src/agents/round-trip.ts +192 -0
  198. package/src/agents/secrets.ts +13 -0
  199. package/src/agents/toolchain.ts +34 -0
  200. package/src/agents/verify-cache.ts +103 -0
  201. package/src/agents/verify.ts +127 -0
  202. package/src/auth-checks.ts +163 -0
  203. package/src/cli/agent-guide.ts +129 -0
  204. package/src/cli/app.ts +122 -0
  205. package/src/cli/cli.ts +28 -0
  206. package/src/cli/command-index.ts +57 -0
  207. package/src/cli/contracts.ts +25 -0
  208. package/src/cli/fleet-reference.ts +17 -0
  209. package/src/cli/fleet-state.ts +499 -0
  210. package/src/cli/fleet-sync-checkout.ts +130 -0
  211. package/src/cli/fleet-sync-enrollment.ts +140 -0
  212. package/src/cli/fleet-sync-forward.ts +134 -0
  213. package/src/cli/fleet-sync-manager.ts +249 -0
  214. package/src/cli/fleet-sync-monitoring.ts +185 -0
  215. package/src/cli/fleet-sync-output.ts +317 -0
  216. package/src/cli/fleet-sync-privilege.ts +27 -0
  217. package/src/cli/fleet-sync-progress.ts +109 -0
  218. package/src/cli/fleet-sync-remote.ts +242 -0
  219. package/src/cli/fleet-sync-report.ts +26 -0
  220. package/src/cli/fleet-sync-schedule.ts +138 -0
  221. package/src/cli/fleet-sync-state.ts +86 -0
  222. package/src/cli/fleet-sync-vault.ts +166 -0
  223. package/src/cli/fleet-sync.ts +375 -0
  224. package/src/cli/fleet.ts +165 -0
  225. package/src/cli/managed-health.ts +219 -0
  226. package/src/cli/node-deployment.ts +34 -0
  227. package/src/cli/node-sync.ts +121 -0
  228. package/src/cli/node-t3.ts +31 -0
  229. package/src/cli/options.ts +141 -0
  230. package/src/cli/probe-source.ts +7 -0
  231. package/src/cli/program.ts +185 -0
  232. package/src/cli/project.ts +51 -0
  233. package/src/cli/register-fleet.ts +296 -0
  234. package/src/cli/registry.ts +185 -0
  235. package/src/cli/repair-apply.ts +67 -0
  236. package/src/cli/repair-guard.ts +11 -0
  237. package/src/cli/repair-inspect.ts +150 -0
  238. package/src/cli/repair-script.ts +76 -0
  239. package/src/cli/repair.ts +209 -0
  240. package/src/cli/roster.ts +105 -0
  241. package/src/cli/search.ts +35 -0
  242. package/src/cli/settings.ts +67 -0
  243. package/src/cli/source-operations.ts +102 -0
  244. package/src/cli/t3/command.ts +90 -0
  245. package/src/cli/t3/register.ts +55 -0
  246. package/src/cli/tailnet.ts +127 -0
  247. package/src/cli/ui-actions.ts +133 -0
  248. package/src/cli/ui-controller.ts +113 -0
  249. package/src/cli/ui-data.ts +106 -0
  250. package/src/cli/ui-execute.ts +163 -0
  251. package/src/cli/ui-repairs.ts +34 -0
  252. package/src/cli/ui-reports.ts +211 -0
  253. package/src/cli/ui-targets.ts +100 -0
  254. package/src/cli/ui-terminal.ts +109 -0
  255. package/src/cli/ui-types.ts +49 -0
  256. package/src/cli/ui-wizard.ts +101 -0
  257. package/src/cli/ui.ts +9 -0
  258. package/src/cli/worker-health-output.ts +37 -0
  259. package/src/config.ts +186 -0
  260. package/src/configure.ts +255 -0
  261. package/src/definition.ts +290 -0
  262. package/src/diagnostics.ts +92 -0
  263. package/src/doctor-check.ts +10 -0
  264. package/src/errors.ts +1 -0
  265. package/src/fingerprint.ts +120 -0
  266. package/src/fleet-config.ts +174 -0
  267. package/src/input-hashes.ts +73 -0
  268. package/src/machine/local-source.ts +52 -0
  269. package/src/machine/lock-error.ts +2 -0
  270. package/src/machine/platform.ts +44 -0
  271. package/src/machine/source.ts +68 -0
  272. package/src/machine/state.ts +173 -0
  273. package/src/machine-health.ts +95 -0
  274. package/src/managed-source.ts +47 -0
  275. package/src/manager-failure.ts +48 -0
  276. package/src/manager-sync.ts +405 -0
  277. package/src/manager.ts +62 -0
  278. package/src/monitoring/agent-evidence.ts +146 -0
  279. package/src/monitoring/apply.ts +183 -0
  280. package/src/monitoring/beszel.ts +198 -0
  281. package/src/monitoring/enroll.ts +111 -0
  282. package/src/monitoring/hub-access.ts +114 -0
  283. package/src/monitoring/hub-backup.ts +121 -0
  284. package/src/monitoring/hub-viewer.ts +142 -0
  285. package/src/monitoring/hub.ts +93 -0
  286. package/src/monitoring/manager-agent.ts +73 -0
  287. package/src/monitoring/manager-hub.ts +248 -0
  288. package/src/monitoring/remote.ts +65 -0
  289. package/src/network-definition.ts +83 -0
  290. package/src/node-converge-linux.ts +374 -0
  291. package/src/node-converge-shared.ts +83 -0
  292. package/src/node-converge.ts +40 -0
  293. package/src/onboard.ts +35 -0
  294. package/src/package-info.ts +45 -0
  295. package/src/platform/ansible-receipt.ts +128 -0
  296. package/src/platform/ansible.ts +66 -0
  297. package/src/platform/darwin.ts +203 -0
  298. package/src/platform/detect.ts +11 -0
  299. package/src/platform/fallback.ts +110 -0
  300. package/src/platform/index.ts +38 -0
  301. package/src/platform/launchd.ts +98 -0
  302. package/src/platform/linux.ts +139 -0
  303. package/src/platform/ps.ts +45 -0
  304. package/src/platform/resource-verify.ts +132 -0
  305. package/src/platform/systemd-user.ts +95 -0
  306. package/src/platform/t3-policy.ts +137 -0
  307. package/src/platform/t3-route.ts +68 -0
  308. package/src/platform/t3-service.ts +75 -0
  309. package/src/platform/types.ts +123 -0
  310. package/src/platform/ubuntu-audit.ts +66 -0
  311. package/src/platform/ubuntu-verify.ts +105 -0
  312. package/src/platform/windows.ts +148 -0
  313. package/src/power-recovery.ts +55 -0
  314. package/src/probe.ts +51 -0
  315. package/src/protocol.ts +119 -0
  316. package/src/proxy-converge.ts +85 -0
  317. package/src/remote-bootstrap.ts +210 -0
  318. package/src/remote-entry.ts +89 -0
  319. package/src/remote-lock.ts +12 -0
  320. package/src/repair-catalog.ts +206 -0
  321. package/src/repair-plan.ts +78 -0
  322. package/src/repair-runner.ts +84 -0
  323. package/src/report.ts +71 -0
  324. package/src/runner.ts +104 -0
  325. package/src/secrets.ts +169 -0
  326. package/src/service-auth.ts +115 -0
  327. package/src/ssh.ts +135 -0
  328. package/src/state/bucket.ts +208 -0
  329. package/src/state/check.ts +74 -0
  330. package/src/state/connection.ts +92 -0
  331. package/src/state/enrollment.ts +45 -0
  332. package/src/state/legacy.ts +89 -0
  333. package/src/state/litestream.ts +143 -0
  334. package/src/state/operator.ts +20 -0
  335. package/src/state/paths.ts +40 -0
  336. package/src/state/roster.ts +62 -0
  337. package/src/state/store.ts +169 -0
  338. package/src/state/sync.ts +352 -0
  339. package/src/sync.ts +104 -0
  340. package/src/t3/accounts.ts +81 -0
  341. package/src/t3/desired.ts +165 -0
  342. package/src/t3/files.ts +81 -0
  343. package/src/t3/names-check.ts +53 -0
  344. package/src/t3/names.ts +155 -0
  345. package/src/t3/plan.ts +82 -0
  346. package/src/t3/providers.ts +74 -0
  347. package/src/t3/reconcile.ts +123 -0
  348. package/src/t3-endpoint.ts +104 -0
  349. package/src/t3-linked-check.ts +123 -0
  350. package/src/tailnet-health.ts +231 -0
  351. package/src/tailnet-matching.ts +37 -0
  352. package/src/tailscale.ts +141 -0
  353. package/src/toolchain-checks.ts +108 -0
  354. package/src/topology.ts +24 -0
  355. package/src/transport.ts +122 -0
  356. package/src/types.ts +99 -0
  357. package/src/vault-config.ts +38 -0
  358. package/src/worker-access-checks.ts +168 -0
  359. package/src/worker-capacity.ts +34 -0
  360. package/src/worker-host-checks.ts +181 -0
  361. package/src/worker-policy.ts +64 -0
  362. package/src/worker-readiness.ts +64 -0
  363. package/src/workstation-checks.ts +29 -0
@@ -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
+ }
@@ -0,0 +1,103 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { receiptDirectory } from '../platform/ansible-receipt';
5
+ import type { Check } from '../types';
6
+
7
+ /** A verified inference or agent session is re-proven at least this often. */
8
+ export const VERIFY_MAX_AGE_MS = 6 * 60 * 60 * 1000;
9
+
10
+ export interface VerifyCache {
11
+ directory?: string;
12
+ now?: number;
13
+ /** Ignore cached results (a live probe failed, or a full check was asked). */
14
+ bypass?: boolean;
15
+ }
16
+
17
+ /** Digest of everything a result depends on (never secrets). */
18
+ export function cacheKey(...parts: (string | object)[]): string {
19
+ const hash = createHash('sha256');
20
+ for (const part of parts)
21
+ hash
22
+ .update(typeof part === 'string' ? part : JSON.stringify(part))
23
+ .update('\0');
24
+ return hash.digest('hex');
25
+ }
26
+
27
+ /** Digest of source files, so a code change re-verifies. */
28
+ export async function sourceKey(paths: string[]): Promise<string> {
29
+ const hash = createHash('sha256');
30
+ for (const path of [...paths].sort())
31
+ hash
32
+ .update(path)
33
+ .update('\0')
34
+ .update(await readFile(path).catch(() => ''));
35
+ return hash.digest('hex');
36
+ }
37
+
38
+ /** Warm verification: a passing check with an unchanged key is reused until it
39
+ * ages out. Only clean passes are stored; a failure, a skip or a quota-limited
40
+ * pass (`reuse` false) clears the entry so the next sync checks it again.
41
+ */
42
+ export async function cachedCheck(
43
+ name: string,
44
+ key: string,
45
+ compute: () => Promise<Check>,
46
+ options: VerifyCache & { reuse?: (check: Check) => boolean } = {}
47
+ ): Promise<Check> {
48
+ const directory = options.directory ?? join(receiptDirectory(), 'verify');
49
+ const path = join(directory, `${name.replace(/[^a-z0-9-]/gi, '_')}.json`);
50
+ const now = options.now ?? Date.now();
51
+ if (!options.bypass) {
52
+ const hit = await fresh(path, key, now);
53
+ if (hit) return hit;
54
+ }
55
+ const check = await compute();
56
+ const reusable =
57
+ check.status === 'pass' && (options.reuse ? options.reuse(check) : true);
58
+ try {
59
+ if (!reusable) await rm(path, { force: true });
60
+ else {
61
+ await mkdir(directory, { recursive: true, mode: 0o700 });
62
+ await writeFile(
63
+ path,
64
+ JSON.stringify({ version: 1, key, checkedAt: now, check }) + '\n',
65
+ { mode: 0o600 }
66
+ );
67
+ }
68
+ } catch {
69
+ /* A missing cache entry only means the next sync verifies again. */
70
+ }
71
+ return check;
72
+ }
73
+
74
+ async function fresh(
75
+ path: string,
76
+ key: string,
77
+ now: number
78
+ ): Promise<Check | null> {
79
+ try {
80
+ const entry = JSON.parse(await readFile(path, 'utf8')) as {
81
+ version?: unknown;
82
+ key?: unknown;
83
+ checkedAt?: unknown;
84
+ check?: Check;
85
+ };
86
+ if (
87
+ entry.version !== 1 ||
88
+ entry.key !== key ||
89
+ typeof entry.checkedAt !== 'number' ||
90
+ now - entry.checkedAt < 0 ||
91
+ now - entry.checkedAt >= VERIFY_MAX_AGE_MS ||
92
+ entry.check?.status !== 'pass'
93
+ )
94
+ return null;
95
+ const minutes = Math.round((now - entry.checkedAt) / 60_000);
96
+ return {
97
+ ...entry.check,
98
+ summary: `${entry.check.summary} (verified ${minutes}m ago; re-proven every 6h or on any change)`,
99
+ };
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
@@ -0,0 +1,127 @@
1
+ import {
2
+ ProviderVerificationError,
3
+ providerHttpFailure,
4
+ } from './provider-errors';
5
+ import { registry, type Selection } from './registry';
6
+ import { proxyKey } from './secrets';
7
+ import { VERIFY_OUTPUT_TOKENS } from './round-trip';
8
+
9
+ export async function verifyProvider(
10
+ selection: Selection,
11
+ smoke: boolean,
12
+ credential?: string
13
+ ) {
14
+ if (selection.profile !== 'proxy')
15
+ return {
16
+ static: 'pass',
17
+ reachability: 'unverified',
18
+ authenticatedModels: 'unverified',
19
+ inference: 'unverified',
20
+ streaming: 'unverified',
21
+ toolCalls: 'unverified',
22
+ t3EndToEnd: 'unverified',
23
+ };
24
+ const key = credential ?? (await proxyKey());
25
+ const headers = {
26
+ Authorization: `Bearer ${key}`,
27
+ 'Content-Type': 'application/json',
28
+ };
29
+ const response = await fetch(registry.proxy.endpoint + '/v1/models', {
30
+ headers,
31
+ redirect: 'error',
32
+ signal: AbortSignal.timeout(15_000),
33
+ });
34
+ if (!response.ok) throw providerHttpFailure(response.status);
35
+ const data = (await response.json()) as { data?: Array<{ id: string }> };
36
+ if (!data.data?.some((model) => model.id === selection.model))
37
+ throw new ProviderVerificationError(
38
+ 'PROVIDER_MODEL_UNAVAILABLE',
39
+ 'Approved model is unavailable; review registry model selection.'
40
+ );
41
+ let inference = 'unverified';
42
+ if (smoke) {
43
+ const { path, body } = inferenceRequest(selection);
44
+ const result = await fetch(registry.proxy.endpoint + path, {
45
+ method: 'POST',
46
+ headers: {
47
+ ...headers,
48
+ 'anthropic-version': '2023-06-01',
49
+ 'x-goog-api-key': key,
50
+ },
51
+ body: JSON.stringify(body),
52
+ redirect: 'error',
53
+ signal: AbortSignal.timeout(30_000),
54
+ });
55
+ if (!result.ok) throw providerHttpFailure(result.status);
56
+ await validateOutput(result, selection.tool);
57
+ inference = 'pass';
58
+ }
59
+ return {
60
+ static: 'pass',
61
+ reachability: 'pass',
62
+ authenticatedModels: 'pass',
63
+ inference,
64
+ streaming: 'unverified',
65
+ toolCalls: 'unverified',
66
+ t3EndToEnd: 'unverified',
67
+ };
68
+ }
69
+
70
+ async function validateOutput(
71
+ result: Response,
72
+ tool: Selection['tool']
73
+ ): Promise<void> {
74
+ const payload = (await result.json()) as {
75
+ content?: unknown[];
76
+ output?: unknown[];
77
+ error?: unknown;
78
+ candidates?: unknown[];
79
+ };
80
+ const output =
81
+ tool === 'gemini' || tool === 'antigravity'
82
+ ? payload.candidates
83
+ : tool === 'claude'
84
+ ? payload.content
85
+ : payload.output;
86
+ if (payload.error || !Array.isArray(output) || output.length === 0)
87
+ throw new ProviderVerificationError(
88
+ 'PROVIDER_OUTPUT_UNVERIFIED',
89
+ 'Inference returned no output; inspect the provider locally.'
90
+ );
91
+ }
92
+
93
+ /** Exact pinned clients use different wire protocols; model discovery is not inference.
94
+ * Output uses the same ceiling as the round trip: reasoning models (Muse) spend
95
+ * dozens of tokens before any text, so a tiny cap returns an empty output.
96
+ */
97
+ export function inferenceRequest(selection: Selection): {
98
+ path: string;
99
+ body: object;
100
+ } {
101
+ // Antigravity's CLI speaks the same Gemini generateContent protocol.
102
+ if (selection.tool === 'gemini' || selection.tool === 'antigravity')
103
+ return {
104
+ path: `/v1beta/models/${encodeURIComponent(selection.model)}:generateContent`,
105
+ body: {
106
+ contents: [{ role: 'user', parts: [{ text: 'Reply OK.' }] }],
107
+ generationConfig: { maxOutputTokens: VERIFY_OUTPUT_TOKENS },
108
+ },
109
+ };
110
+ if (selection.tool === 'claude')
111
+ return {
112
+ path: '/v1/messages',
113
+ body: {
114
+ model: selection.model,
115
+ max_tokens: VERIFY_OUTPUT_TOKENS,
116
+ messages: [{ role: 'user', content: 'Reply OK.' }],
117
+ },
118
+ };
119
+ return {
120
+ path: '/v1/responses',
121
+ body: {
122
+ model: selection.model,
123
+ max_output_tokens: VERIFY_OUTPUT_TOKENS,
124
+ input: 'Reply OK.',
125
+ },
126
+ };
127
+ }
@@ -0,0 +1,163 @@
1
+ import { resolveVaultConfig, usesVault } from './vault-config';
2
+ import { fleet as definition } from './fleet-config';
3
+ import { workerReadiness } from './worker-readiness';
4
+ import { record } from './config';
5
+ import { codingAuth, serviceAuth } from './service-auth';
6
+ import { workstationChecks } from './workstation-checks';
7
+ import { toolchainChecks } from './toolchain-checks';
8
+ import type { Capability, Check, Runner, CommandResult } from './types';
9
+
10
+ export async function authCheck(
11
+ capability: Capability,
12
+ run: Runner,
13
+ root: string,
14
+ nodeId?: string
15
+ ): Promise<Check[]> {
16
+ if (capability === 'worker') return workerReadiness(run, nodeId, root);
17
+ if (capability === 't3') return t3Checks(run, nodeId !== undefined);
18
+ if (capability === 'codex' || capability === 'claude')
19
+ return codingAuth(capability, run);
20
+ if (capability === 'services') return serviceAuth(run);
21
+ if (capability === 'workstation') return workstationChecks(root, run);
22
+ if (capability === 'toolchain')
23
+ return toolchainChecks(
24
+ root,
25
+ run,
26
+ definition.workers.some((node) => node.id === nodeId)
27
+ );
28
+ return basicAuth(capability, run, root);
29
+ }
30
+
31
+ async function basicAuth(
32
+ capability: 'github' | 'vault' | 'vscode',
33
+ run: Runner,
34
+ root: string
35
+ ): Promise<Check[]> {
36
+ const spec = {
37
+ github: {
38
+ command: ['gh', 'auth', 'status', '--active'],
39
+ action:
40
+ 'Authenticate on this node with gh auth login, then gh auth setup-git.',
41
+ },
42
+ vault: {
43
+ command: [
44
+ 'vault',
45
+ 'token',
46
+ 'lookup',
47
+ '-format=json',
48
+ `-address=${usesVault() ? resolveVaultConfig().addr : ''}`,
49
+ ],
50
+ action:
51
+ 'Authenticate to the configured Vault on this node. Do not copy a token from another computer.',
52
+ },
53
+ vscode: {
54
+ command: ['code', '--list-extensions'],
55
+ action:
56
+ 'Install the VS Code CLI on PATH and ms-vscode-remote.remote-ssh in the local editor profile.',
57
+ },
58
+ }[capability];
59
+ const result = await run(spec.command, {
60
+ cwd: root,
61
+ timeoutMs: 15_000,
62
+ });
63
+ const extensions = result.stdout.toLowerCase().split(/\s+/);
64
+ const ok =
65
+ result.ok &&
66
+ (capability !== 'vscode' ||
67
+ extensions.includes('ms-vscode-remote.remote-ssh'));
68
+ return [
69
+ {
70
+ id: `auth:${capability}`,
71
+ status: ok ? 'pass' : 'fail',
72
+ summary: ok
73
+ ? `${capability} check succeeded in this process context.`
74
+ : `${capability} check failed (${result.reason}); no credential output retained.`,
75
+ ...(ok ? {} : { remedy: { action: spec.action } }),
76
+ },
77
+ ];
78
+ }
79
+
80
+ async function t3Checks(
81
+ run: Runner,
82
+ managerChecksLinks = false
83
+ ): Promise<Check[]> {
84
+ const t3 = await t3Executable(run);
85
+ const config = await run([t3, 'status', '--json']);
86
+ const checks: Check[] = [t3Settings(config)];
87
+ const live = await run([t3, 'environment', 'list', '--json']);
88
+ checks.push({
89
+ id: 't3:api',
90
+ status: live.ok ? 'pass' : 'fail',
91
+ summary: live.ok
92
+ ? 'Local T3 API answered a read-only environment listing; peer connectivity is not inferred.'
93
+ : 'Local T3 API did not answer the environment listing.',
94
+ ...(!live.ok
95
+ ? {
96
+ remedy: {
97
+ action:
98
+ 'Check t3 service status and t3 status. Inspect linked peers in T3 Settings → Connections; relink only with operator approval.',
99
+ },
100
+ }
101
+ : {}),
102
+ });
103
+ if (!managerChecksLinks)
104
+ checks.push({
105
+ id: 't3:end-to-end',
106
+ status: 'skip',
107
+ summary:
108
+ 'Relay traversal and remote peer authorization require an operator connection test; local settings are not proof.',
109
+ });
110
+ return checks;
111
+ }
112
+
113
+ /** Workers run only the T3 desktop app, so no `t3` launcher need be on PATH.
114
+ * Fall back to the newest pinned native CLI the shared installer verified.
115
+ */
116
+ async function t3Executable(run: Runner): Promise<string> {
117
+ const found = await run(
118
+ [
119
+ 'sh',
120
+ '-c',
121
+ 'command -v t3 || ls -d "$HOME"/.local/share/personal/tools/t3-*/t3 2>/dev/null | sort -V | tail -n 1',
122
+ ],
123
+ { timeoutMs: 5000 }
124
+ );
125
+ const path = found.ok ? (found.stdout.trim().split('\n').pop() ?? '') : '';
126
+ return /^\/[\x21-\x7e]+$/.test(path) ? path : 't3';
127
+ }
128
+
129
+ function t3Settings(config: CommandResult): Check {
130
+ const fallback: Check = {
131
+ id: 't3:settings',
132
+ status: 'fail',
133
+ summary: 'T3 settings unavailable or unsupported CLI response.',
134
+ remedy: {
135
+ action:
136
+ 'Run t3 status --json on this node. Complete t3 up --connect as its operator if needed.',
137
+ },
138
+ };
139
+ try {
140
+ const data: unknown = JSON.parse(config.stdout);
141
+ if (config.ok && record(data) && record(data['connect'])) {
142
+ const enabled =
143
+ data['connect']['enabled'] === true &&
144
+ data['connect']['revoked'] === false;
145
+ return {
146
+ id: 't3:settings',
147
+ status: enabled ? 'pass' : 'fail',
148
+ summary: enabled
149
+ ? 'T3 Connect enabled and not marked revoked (settings only).'
150
+ : 'T3 Connect disabled, revoked, or unknown.',
151
+ ...(!enabled
152
+ ? {
153
+ remedy: {
154
+ action:
155
+ 'Review t3 status, then complete t3 up --connect as the operator. Never share pairing tokens.',
156
+ },
157
+ }
158
+ : {}),
159
+ };
160
+ }
161
+ } catch {}
162
+ return fallback;
163
+ }