@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,185 @@
1
+ name: CLI proxy health
2
+ # One workflow, five entry points, deliberately cheap by default:
3
+ # every 15 min readiness: free (no inference) + cloud checkpoint freshness
4
+ # hourly light: 2 tiny requests per model (about 200 requests a day)
5
+ # 03:41 nightly comprehensive suite (bounded by models.json budgets)
6
+ # pull request contract: offline, no credentials, no inference
7
+ # manual any profile, one model or one check, optional force-probe
8
+ on:
9
+ schedule:
10
+ - cron: '7,22,37,52 * * * *'
11
+ - cron: '17 * * * *'
12
+ - cron: '41 3 * * *'
13
+ pull_request:
14
+ paths:
15
+ - cliproxy/**
16
+ - .github/workflows/cliproxy-health.yml
17
+ workflow_dispatch:
18
+ inputs:
19
+ profile:
20
+ description: readiness is free; light is the hourly check; nightly is comprehensive; manual adds wrong-key probes and fault simulations.
21
+ type: choice
22
+ default: light
23
+ options: [liveness, readiness, light, nightly, manual]
24
+ suite:
25
+ description: Which declared checks to run.
26
+ type: choice
27
+ default: all
28
+ options: [all, core, cursor]
29
+ model:
30
+ description: Optional single model id from cliproxy/models.json (partial runs never open or close the tracking issue).
31
+ type: string
32
+ default: ''
33
+ check:
34
+ description: Optional capability glob such as chat.tools or responses.* (cheap reproduction of one finding).
35
+ type: string
36
+ default: ''
37
+ max_requests:
38
+ description: Optional lower request budget for this run.
39
+ type: string
40
+ default: ''
41
+ force_probe:
42
+ description: Probe models even if they were rate limited recently (normally paused to protect quota).
43
+ type: boolean
44
+ default: false
45
+ permissions:
46
+ contents: read
47
+ concurrency:
48
+ group: cliproxy-model-health-${{ github.event_name == 'pull_request' && format('pr-{0}', github.ref) || github.event_name == 'workflow_dispatch' && inputs.profile || github.event.schedule }}
49
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
50
+ env:
51
+ PROFILE: ${{ github.event_name == 'workflow_dispatch' && inputs.profile || github.event.schedule == '41 3 * * *' && 'nightly' || github.event.schedule == '17 * * * *' && 'light' || 'readiness' }}
52
+ jobs:
53
+ contract:
54
+ # Pull requests: deterministic protocol, routing and report contracts. No credentials, no inference.
55
+ if: github.event_name == 'pull_request'
56
+ runs-on: ubuntu-latest
57
+ timeout-minutes: 10
58
+ steps:
59
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
60
+ - name: Install PyYAML for the validate-config contract
61
+ run: |
62
+ sudo apt-get update
63
+ sudo apt-get install -y --no-install-recommends python3-yaml
64
+ - name: Lint models.json and run the offline contract tests
65
+ run: |
66
+ python3 -B cliproxy/scripts/check.py
67
+ python3 -B -m unittest discover -s cliproxy/tests -p "test_model_*.py"
68
+ - name: Run the full declarative suite against the local emulator
69
+ run: |
70
+ set +e
71
+ python3 -B cliproxy/scripts/model_health.py model-suite --target mock --profile manual \
72
+ --fail-on degraded --output model-health-report --seed 1 > /dev/null
73
+ code=$?
74
+ if [ -f model-health-report/model-health.md ]; then cat model-health-report/model-health.md >> "$GITHUB_STEP_SUMMARY"; fi
75
+ exit $code
76
+ - name: Keep structured report
77
+ if: ${{ always() && hashFiles('model-health-report/model-health.json') != '' }}
78
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
79
+ with:
80
+ name: model-health-contract-${{ github.run_id }}
81
+ path: model-health-report
82
+ retention-days: 14
83
+ health:
84
+ if: github.event_name != 'pull_request' && github.repository == 'crvouga/dev-fleet' && github.ref == 'refs/heads/main'
85
+ runs-on: ubuntu-latest
86
+ timeout-minutes: 60
87
+ permissions:
88
+ contents: read
89
+ id-token: write
90
+ issues: write
91
+ steps:
92
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
93
+ - name: Install read-only backup client
94
+ if: env.PROFILE == 'readiness'
95
+ run: |
96
+ sudo apt-get update
97
+ sudo apt-get install -y --no-install-recommends restic
98
+ - name: Read-only Vault login using GitHub OIDC
99
+ uses: hashicorp/vault-action@892a26828f195e65540a40b4768ae4571f51ebfc # v4.0.0
100
+ with:
101
+ url: https://vault.example.com
102
+ method: jwt
103
+ path: jwt
104
+ role: github-actions
105
+ jwtGithubAudience: https://vault.example.com
106
+ exportToken: true
107
+ - name: Verify endpoint and cloud recovery freshness
108
+ if: env.PROFILE == 'readiness'
109
+ run: python3 -B cliproxy/scripts/external-health.py
110
+ - name: Restore last-success and rate-limit state
111
+ uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
112
+ with:
113
+ path: model-health-state.json
114
+ key: cliproxy-model-health-state-${{ github.run_id }}
115
+ restore-keys: cliproxy-model-health-state-
116
+ - name: Validate every required model
117
+ # Bounded spend: the profile's request/token budget, concurrency and timeouts come from models.json;
118
+ # inference is never retried; a model rate limited recently is not probed again until its pause ends.
119
+ # The client key is read in-process from Vault and never printed.
120
+ if: ${{ !cancelled() }}
121
+ env:
122
+ MODEL: ${{ inputs.model }}
123
+ CHECK: ${{ inputs.check }}
124
+ MAX_REQUESTS: ${{ inputs.max_requests }}
125
+ SUITE: ${{ inputs.suite || 'all' }}
126
+ FORCE_PROBE: ${{ inputs.force_probe }}
127
+ run: |
128
+ command=model-health
129
+ fail_on=unhealthy
130
+ case "$PROFILE" in nightly | manual) command=model-suite; fail_on=degraded ;; esac
131
+ args=(--profile "$PROFILE" --suite "$SUITE" --fail-on "$fail_on"
132
+ --output model-health-report --state model-health-state.json)
133
+ if [ -n "$MODEL" ]; then args+=(--model "$MODEL"); fi
134
+ if [ -n "$CHECK" ]; then args+=(--check "$CHECK"); fi
135
+ if [ -n "$MAX_REQUESTS" ]; then args+=(--max-requests "$MAX_REQUESTS"); fi
136
+ if [ "$FORCE_PROBE" = "true" ]; then args+=(--force-probe); fi
137
+ set +e
138
+ python3 -B cliproxy/scripts/model_health.py "$command" "${args[@]}" > /dev/null
139
+ code=$?
140
+ if [ -f model-health-report/agent-brief.md ]; then cat model-health-report/agent-brief.md >> "$GITHUB_STEP_SUMMARY"; fi
141
+ if [ -f model-health-report/model-health.md ]; then cat model-health-report/model-health.md >> "$GITHUB_STEP_SUMMARY"; fi
142
+ exit $code
143
+ - name: Save last-success and rate-limit state
144
+ if: ${{ always() && hashFiles('model-health-state.json') != '' }}
145
+ uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
146
+ with:
147
+ path: model-health-state.json
148
+ key: cliproxy-model-health-state-${{ github.run_id }}
149
+ - name: Keep structured report
150
+ if: ${{ always() && hashFiles('model-health-report/model-health.json') != '' }}
151
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
152
+ with:
153
+ name: model-health-${{ env.PROFILE }}-${{ github.run_id }}
154
+ path: model-health-report
155
+ retention-days: 30
156
+ - name: Track findings in one issue per scope
157
+ # Opens or updates a single tracking issue when the findings change, and closes it when a full
158
+ # run is healthy. The body is the redacted agent brief: structure and numbers, no prompts or keys.
159
+ if: ${{ always() && hashFiles('model-health-report/model-health.json') != '' && inputs.model == '' && inputs.check == '' }}
160
+ env:
161
+ GH_TOKEN: ${{ github.token }}
162
+ RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
163
+ run: |
164
+ scope=live
165
+ case "$PROFILE" in nightly | manual) scope=nightly ;; esac
166
+ title="[ops] CLI proxy model health ($scope)"
167
+ report=model-health-report/model-health.json
168
+ status=$(jq -r .summary.status "$report")
169
+ fingerprint=$(jq -r .summary.findings.fingerprint "$report")
170
+ number=$(gh issue list --state open --search "\"$title\" in:title" --json number,title \
171
+ --jq "map(select(.title==\"$title\"))[0].number // empty")
172
+ if [ "$status" = unhealthy ] || [ "$status" = degraded ]; then
173
+ {
174
+ head -c 58000 model-health-report/agent-brief.md
175
+ printf '\n\nRun: %s (artifact model-health-%s-%s)\n<!-- fingerprint: %s -->\n' "$RUN_URL" "$PROFILE" "$GITHUB_RUN_ID" "$fingerprint"
176
+ } > model-health-report/issue.md
177
+ if [ -z "$number" ]; then
178
+ gh issue create --title "$title" --body-file model-health-report/issue.md
179
+ elif ! gh issue view "$number" --json body --jq .body | grep -q "fingerprint: $fingerprint"; then
180
+ gh issue edit "$number" --body-file model-health-report/issue.md
181
+ gh issue comment "$number" --body "Findings changed: $RUN_URL"
182
+ fi
183
+ elif [ "$status" = healthy ] && [ -n "$number" ]; then
184
+ gh issue close "$number" --comment "Healthy again: $RUN_URL"
185
+ fi
@@ -0,0 +1,131 @@
1
+ # Restore and migration
2
+
3
+ Exactly one proxy instance runs, on the **manager** (`manager.id` in `fleet.json`). Workers are clients and never host it. Never activate duplicated OAuth or Tailscale identities concurrently. Source code comes from this package at a reviewed Git revision; secrets and state come from Vault and encrypted R2 snapshots.
4
+
5
+ `INSTALL_ROOT` below is `~/Library/Application Support/cliproxy-fleet` on the manager and `/opt/cliproxy-fleet` on the legacy Linux host. Run every `devfleet cliproxy …` command from a repository checkout on the host named in the step.
6
+
7
+ ## Move the proxy from a Linux worker to the manager
8
+
9
+ Today the proxy still runs on the worker `worker-one` (legacy systemd, `/opt/cliproxy-fleet`). This one-time, operator-approved move fences that host and recovers its latest checkpoint on the manager `manager-mac`. The acknowledgement flags below are operator fencing, not a distributed lock. Plan a short outage: workers have no proxy between the source stop and the manager health check.
10
+
11
+ ### 1. Prepare the manager (no outage)
12
+
13
+ On the manager, logged in, on AC power, with Docker Desktop running (enable **Start Docker Desktop when you sign in**):
14
+
15
+ ```bash
16
+ cliproxy/scripts/install-dependencies
17
+ devfleet cliproxy install --prepare-only --revision HEAD
18
+ devfleet cliproxy vault-login
19
+ devfleet cliproxy recover
20
+ devfleet cliproxy snapshots
21
+ ```
22
+
23
+ `install-dependencies` builds the pinned tools directory (mise Python + hash-locked PyYAML, mise `restic`) that the LaunchAgents run with; it never uses sudo or Homebrew. `--prepare-only` creates the worktree and private directories without starting anything. `recover` reads the existing Restic password from Vault and confirms it; it never generates or writes cloud secrets. Do not run `initialize` before recovery.
24
+
25
+ ### 2. Fence the source (outage starts)
26
+
27
+ On `worker-one`, stop scheduling, drain the dedicated Tailscale Service advertisement, stop and disable the stack, then take the final consistent checkpoint:
28
+
29
+ ```bash
30
+ sudo systemctl disable --now cliproxy-backup.timer cliproxy-backup-check.timer cliproxy-gc.timer
31
+ devfleet cliproxy drain
32
+ devfleet cliproxy stop
33
+ sudo systemctl disable cliproxy-fleet.service
34
+ devfleet cliproxy backup
35
+ devfleet cliproxy snapshots
36
+ sudo systemctl disable --now cliproxy-monitor.timer cliproxy-vault-renew.timer
37
+ ```
38
+
39
+ An already stopped stack remains stopped after backup. Record the **latest successful snapshot ID** from `/opt/cliproxy-fleet/state/status/backup-success.json`; do not choose an incomplete snapshot from a failed backup. Confirm the fence before continuing:
40
+
41
+ ```bash
42
+ systemctl is-active cliproxy-fleet.service # inactive
43
+ systemctl is-enabled cliproxy-fleet.service # disabled
44
+ python3 -B cliproxy/scripts/fleet_sync.py --definition fleet.json \
45
+ --node worker-one --revision "$(git rev-parse HEAD)" # PROXY_DECOMMISSION_PENDING, not PROXY_DUPLICATE_OWNER
46
+ ```
47
+
48
+ Keep the source powered off or fenced until the move is verified or rolled back.
49
+
50
+ ### 3. Recover and start on the manager
51
+
52
+ On the manager:
53
+
54
+ ```bash
55
+ devfleet cliproxy restore-cache --previous-node-stopped
56
+ devfleet cliproxy install --revision HEAD
57
+ devfleet cliproxy audit
58
+ devfleet cliproxy health
59
+ devfleet cliproxy activate
60
+ devfleet cliproxy public-endpoint verify
61
+ python3 -B cliproxy/tests/live.py --endpoint https://cli-proxy.example.com --codex
62
+ devfleet cliproxy backup
63
+ ```
64
+
65
+ `restore-cache` downloads the latest modern checkpoint, validates it and populates the empty cache without starting services. `install` (never sudo on macOS) regenerates secret caches from Vault, records the user's UID/GID for Compose, links and loads the `dev.devfleet.cliproxy-*` LaunchAgents in `gui/$UID`, and starts the recovered stack; it refuses missing runtime config and stops with `DOCKER_DESKTOP_NOT_RUNNING` if Docker Desktop is down. `audit` must show clean provenance and loaded agents. `activate` resumes the existing `svc:ai-proxy` advertisement from the restored dedicated identity. A restored `state/public-endpoint-enabled` marker starts the public connector automatically; otherwise run `devfleet cliproxy public-endpoint enable` first.
66
+
67
+ Then confirm the fleet view and clients:
68
+
69
+ ```bash
70
+ python3 -B cliproxy/scripts/fleet_sync.py --definition fleet.json \
71
+ --node manager-mac --revision "$(git rev-parse HEAD)" # ok: true, role manager, backend launchd
72
+ ```
73
+
74
+ From each worker, run the client bootstrap's `--validate` and an actual client/tool request through `https://cli-proxy.example.com` and `https://ai-proxy.tail0a1b2c.ts.net`. Remove temporary plaintext restore copies after verification.
75
+
76
+ ### 4. Decommission the source
77
+
78
+ After the manager has run cleanly through several scheduled backups, retire the old host's installation. It contains private provider/Tailscale state, so remove it rather than leaving it enabled:
79
+
80
+ ```bash
81
+ sudo docker compose --project-directory /opt/cliproxy-fleet \
82
+ --profile portable-endpoint --profile public-endpoint down
83
+ sudo rm /etc/systemd/system/cliproxy-*.service /etc/systemd/system/cliproxy-*.timer
84
+ sudo systemctl daemon-reload
85
+ sudo rm -rf /opt/cliproxy-fleet
86
+ git worktree prune
87
+ ```
88
+
89
+ `fleet_sync.py --node worker-one` then reports `PROXY_CLIENT_ROLE`. Fleet-sync never performs these steps itself.
90
+
91
+ ### Rollback a failed move
92
+
93
+ 1. On the manager, stop and unload it, keeping any newer state:
94
+
95
+ ```bash
96
+ devfleet cliproxy drain
97
+ devfleet cliproxy stop
98
+ devfleet cliproxy backup # only if the manager refreshed credentials or changed config
99
+ for label in fleet monitor vault-renew backup backup-check gc; do
100
+ launchctl bootout "gui/$(id -u)/dev.devfleet.cliproxy-$label"
101
+ launchctl disable "gui/$(id -u)/dev.devfleet.cliproxy-$label"
102
+ done
103
+ ```
104
+
105
+ Move `INSTALL_ROOT/state/runtime`, `state/tailscale` and `state/endpoint.json` into a private directory under `INSTALL_ROOT/recovery/`. With the runtime cache gone, `fleet-sync` reports `PROXY_RECOVERY_REQUIRED` and can never restart the manager copy.
106
+
107
+ 2. On the source, if the manager took a newer backup, move the source's `state/runtime`, `state/tailscale` and `state/endpoint.json` aside the same way and run `devfleet cliproxy restore-cache --previous-node-stopped`; otherwise keep the original source state (old OAuth refresh tokens may be invalid after the manager refreshed them).
108
+ 3. Restart the source with `sudo systemctl enable cliproxy-fleet.service`, `devfleet cliproxy install --revision REV` (the revision it ran before), `devfleet cliproxy activate`, then re-enable its timers with `sudo systemctl enable --now cliproxy-monitor.timer cliproxy-vault-renew.timer cliproxy-backup.timer cliproxy-backup-check.timer cliproxy-gc.timer`.
109
+ 4. Verify the stable endpoints from fleet clients. Until the move is retried, fleet-sync reports `PROXY_DUPLICATE_OWNER` on the source; that is expected and is never auto-remediated.
110
+
111
+ ## One-command offline recovery
112
+
113
+ On a host with the tracked package installed and a valid private Vault token, `devfleet cliproxy recover-node NEW_DIRECTORY` builds a complete new cache using fresh Vault secrets and a verified R2 checkpoint. Supply `--vault-token-file FILE` to use an explicit existing login. It refuses existing destinations and never changes or stops the running stack. This is also a safe recovery rehearsal while the manager remains online: the new identity files are never activated. Remove rehearsal caches after inspection, since they contain private credentials.
114
+
115
+ On a replacement manager without installed tooling, invoke `python3 -B cliproxy/scripts/recover_node.py "$HOME/Library/Application Support/cliproxy-fleet" --vault-token-file FILE` from a full Git clone after `install-dependencies` passes. The destination must not yet exist. After explicitly fencing the old host, `devfleet cliproxy install --revision REV` installs tracked code and activates the recovered cache. This route replaces the separate prepare/login/password/restore steps above when an existing Vault token is already available. It does not obtain a new machine identity or bypass old-node fencing.
116
+
117
+ ## Recovery details
118
+
119
+ Recovery uses the current trusted Git checkout, validates decrypted data, and preserves the new Vault login and independently recovered Restic password. It never executes backed-up code. To reproduce the original software version, read the recovered checkpoint's `INSTALLATION.json` in an offline restore first and choose that reviewed Git revision instead of HEAD.
120
+
121
+ `restore-cache` intentionally refuses an existing `runtime`, `tailscale` or endpoint cache. An existing repository pin must match the restored destination. For recovery on the same host, stop the service/agents and move these cache entries to a private rollback directory first. Never move `state/status/operation.lock` while an operation is active. An interrupted recovery leaves `state/recovery-incomplete`; inspect/remove the partial cache before retrying. Keep `state/secrets` (new Vault login/password) and the lock directory intact.
122
+
123
+ For offline inspection or historical recovery, use `devfleet cliproxy restore SNAPSHOT_ID EMPTY_DIRECTORY`, then `python3 -B INSTALL_ROOT/scripts/restore-verify EMPTY_DIRECTORY`. Modern checkpoints are located automatically despite different source paths. Old snapshots without `CHECKPOINT.json` require the original explicit restored deployment path and manual state recovery. No automatic fallback to an older snapshot occurs when validation fails.
124
+
125
+ The existing Tailscale Service remains in the cloud. Reuse the restored dedicated identity only while the original is stopped. If a drained identity requires re-advertising, use the pinned Tailscale CLI's `serve advertise` command for `svc:ai-proxy`, then inspect `serve status`. Do not change the manager's ordinary Tailscale identity or assume hostnames transfer. A new tagged identity/service definition requires the platform's OpenTofu-managed enrollment/approval workflow; no cloud provisioning helper exists in this package.
126
+
127
+ A normal code/image rollback uses `devfleet cliproxy install --rollback`. That restores a recorded Git revision, not application data. For incompatible state migrations, restore a matching pre-upgrade snapshot while stopped.
128
+
129
+ ## Public hostname cutover
130
+
131
+ The public DNS/tunnel resources remain owned by OpenTofu. A restored `state/public-endpoint-enabled` marker makes service startup fetch the current isolated tunnel credential from Vault and start the connector automatically. The tunnel token itself is excluded from snapshots. If the marker is absent, run `devfleet cliproxy public-endpoint enable` after restoring and stopping the old host. Verify with `devfleet cliproxy public-endpoint verify` and `tests/live.py --endpoint https://cli-proxy.example.com --codex`. Client API and management keys do not change; keep the old host stopped during recovery.
@@ -0,0 +1,364 @@
1
+ {
2
+ "$comment": "Subset of JSON Schema checked by scripts/modelhealth/schema.py in tests/test_model_agent_report.py. Report format: cliproxy-model-health/1.",
3
+ "type": "object",
4
+ "required": [
5
+ "schema",
6
+ "generated_at",
7
+ "profile",
8
+ "suite",
9
+ "target",
10
+ "seed",
11
+ "levels",
12
+ "models",
13
+ "simulations",
14
+ "summary",
15
+ "findings",
16
+ "routing",
17
+ "cursor"
18
+ ],
19
+ "properties": {
20
+ "schema": {
21
+ "type": "string",
22
+ "const": "cliproxy-model-health/1"
23
+ },
24
+ "generated_at": {
25
+ "type": "string"
26
+ },
27
+ "profile": {
28
+ "type": "string",
29
+ "enum": ["liveness", "readiness", "light", "nightly", "manual"]
30
+ },
31
+ "suite": {
32
+ "type": "string",
33
+ "enum": ["core", "cursor", "all"]
34
+ },
35
+ "target": {
36
+ "type": "object",
37
+ "required": ["kind", "base_url"],
38
+ "properties": {
39
+ "kind": {
40
+ "type": "string",
41
+ "enum": ["live", "mock"]
42
+ },
43
+ "base_url": {
44
+ "type": "string"
45
+ }
46
+ }
47
+ },
48
+ "seed": {
49
+ "type": "integer"
50
+ },
51
+ "levels": {
52
+ "type": "object",
53
+ "required": ["liveness"],
54
+ "properties": {
55
+ "liveness": {
56
+ "type": "object",
57
+ "required": ["status", "category", "details"],
58
+ "properties": {
59
+ "status": {
60
+ "type": "string",
61
+ "enum": ["healthy", "degraded", "unhealthy", "unknown"]
62
+ },
63
+ "category": {
64
+ "type": ["string", "null"]
65
+ },
66
+ "details": {
67
+ "type": "array",
68
+ "items": {
69
+ "type": "string"
70
+ }
71
+ }
72
+ }
73
+ },
74
+ "readiness": {
75
+ "type": "object",
76
+ "required": ["status", "category", "details"],
77
+ "properties": {
78
+ "status": {
79
+ "type": "string",
80
+ "enum": ["healthy", "degraded", "unhealthy", "unknown"]
81
+ },
82
+ "category": {
83
+ "type": ["string", "null"]
84
+ },
85
+ "details": {
86
+ "type": "array",
87
+ "items": {
88
+ "type": "string"
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+ },
95
+ "models": {
96
+ "type": "array",
97
+ "items": {
98
+ "type": "object",
99
+ "required": [
100
+ "id",
101
+ "provider",
102
+ "status",
103
+ "reasons",
104
+ "coverage",
105
+ "capabilities",
106
+ "checks",
107
+ "metrics",
108
+ "rate_limited",
109
+ "paused",
110
+ "last_success"
111
+ ],
112
+ "properties": {
113
+ "id": {
114
+ "type": "string"
115
+ },
116
+ "provider": {
117
+ "type": "string"
118
+ },
119
+ "status": {
120
+ "type": "string",
121
+ "enum": ["healthy", "degraded", "unhealthy", "unknown"]
122
+ },
123
+ "reasons": {
124
+ "type": "array",
125
+ "items": {
126
+ "type": "string"
127
+ }
128
+ },
129
+ "coverage": {
130
+ "type": "object",
131
+ "required": ["capabilities", "exercised", "by_result"]
132
+ },
133
+ "capabilities": {
134
+ "type": "object"
135
+ },
136
+ "checks": {
137
+ "type": "array",
138
+ "items": {
139
+ "type": "object",
140
+ "required": [
141
+ "id",
142
+ "capability",
143
+ "status",
144
+ "category",
145
+ "severity",
146
+ "endpoint",
147
+ "details",
148
+ "duration_ms"
149
+ ],
150
+ "properties": {
151
+ "status": {
152
+ "type": "string",
153
+ "enum": ["pass", "fail", "untested"]
154
+ },
155
+ "severity": {
156
+ "type": "string",
157
+ "enum": ["critical", "major", "minor"]
158
+ }
159
+ }
160
+ }
161
+ },
162
+ "metrics": {
163
+ "type": "object",
164
+ "required": [
165
+ "requests",
166
+ "availability",
167
+ "success_rate",
168
+ "ttft_ms",
169
+ "latency_ms",
170
+ "streaming_completion_rate",
171
+ "tool_call_success_rate",
172
+ "error_categories",
173
+ "upstream_failures",
174
+ "resolved_models"
175
+ ]
176
+ },
177
+ "rate_limited": {
178
+ "type": "boolean"
179
+ },
180
+ "paused": {
181
+ "type": "boolean"
182
+ },
183
+ "last_success": {
184
+ "type": "object"
185
+ },
186
+ "rate_limit_detail": {
187
+ "type": ["string", "null"]
188
+ }
189
+ }
190
+ }
191
+ },
192
+ "simulations": {
193
+ "type": "array",
194
+ "items": {
195
+ "type": "object",
196
+ "required": ["id", "capability", "status", "evidence", "details"],
197
+ "properties": {
198
+ "status": {
199
+ "type": "string",
200
+ "enum": ["pass", "fail"]
201
+ },
202
+ "evidence": {
203
+ "type": "string",
204
+ "const": "simulated"
205
+ }
206
+ }
207
+ }
208
+ },
209
+ "summary": {
210
+ "type": "object",
211
+ "required": [
212
+ "status",
213
+ "levels",
214
+ "models",
215
+ "rate_limited",
216
+ "simulation_failures",
217
+ "findings"
218
+ ],
219
+ "properties": {
220
+ "rate_limited": {
221
+ "type": "array",
222
+ "items": {
223
+ "type": "string"
224
+ }
225
+ },
226
+ "status": {
227
+ "type": "string",
228
+ "enum": ["healthy", "degraded", "unhealthy", "unknown"]
229
+ },
230
+ "findings": {
231
+ "type": "object",
232
+ "required": ["total", "operator", "agent", "fingerprint"],
233
+ "properties": {
234
+ "total": {
235
+ "type": "integer",
236
+ "minimum": 0
237
+ },
238
+ "operator": {
239
+ "type": "integer",
240
+ "minimum": 0
241
+ },
242
+ "agent": {
243
+ "type": "integer",
244
+ "minimum": 0
245
+ },
246
+ "fingerprint": {
247
+ "type": "string",
248
+ "minLength": 8
249
+ }
250
+ }
251
+ }
252
+ }
253
+ },
254
+ "findings": {
255
+ "type": "array",
256
+ "items": {
257
+ "type": "object",
258
+ "required": [
259
+ "id",
260
+ "status",
261
+ "severity",
262
+ "category",
263
+ "model",
264
+ "check",
265
+ "endpoint",
266
+ "summary",
267
+ "owner",
268
+ "resolver",
269
+ "agent_fixable",
270
+ "evidence",
271
+ "reproduce",
272
+ "fix_hints"
273
+ ],
274
+ "properties": {
275
+ "id": {
276
+ "type": "string",
277
+ "minLength": 3
278
+ },
279
+ "status": {
280
+ "type": "string",
281
+ "enum": ["fail", "paused", "untested"]
282
+ },
283
+ "severity": {
284
+ "type": "string",
285
+ "enum": ["critical", "major", "minor", "info"]
286
+ },
287
+ "category": {
288
+ "type": "string"
289
+ },
290
+ "model": {
291
+ "type": ["string", "null"]
292
+ },
293
+ "check": {
294
+ "type": ["string", "null"]
295
+ },
296
+ "endpoint": {
297
+ "type": ["string", "null"]
298
+ },
299
+ "summary": {
300
+ "type": "string",
301
+ "minLength": 1
302
+ },
303
+ "owner": {
304
+ "type": "string"
305
+ },
306
+ "resolver": {
307
+ "type": "string",
308
+ "enum": ["operator", "agent"]
309
+ },
310
+ "agent_fixable": {
311
+ "type": "boolean"
312
+ },
313
+ "evidence": {
314
+ "type": "object",
315
+ "required": ["details", "trace"],
316
+ "properties": {
317
+ "details": {
318
+ "type": "array",
319
+ "items": {
320
+ "type": "string"
321
+ }
322
+ },
323
+ "trace": {
324
+ "type": "array",
325
+ "items": {
326
+ "type": "object"
327
+ }
328
+ }
329
+ }
330
+ },
331
+ "reproduce": {
332
+ "type": "string",
333
+ "minLength": 10
334
+ },
335
+ "fix_hints": {
336
+ "type": "array",
337
+ "items": {
338
+ "type": "string"
339
+ }
340
+ }
341
+ }
342
+ }
343
+ },
344
+ "routing": {
345
+ "type": "object",
346
+ "required": ["declared", "live_verified"],
347
+ "properties": {
348
+ "live_verified": {
349
+ "type": "boolean"
350
+ }
351
+ }
352
+ },
353
+ "cursor": {
354
+ "type": "object",
355
+ "required": ["application_verified"],
356
+ "properties": {
357
+ "application_verified": {
358
+ "type": "boolean",
359
+ "const": false
360
+ }
361
+ }
362
+ }
363
+ }
364
+ }