@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,131 @@
1
+ """Build protocol-specific request bodies from protocol-neutral turn fixtures."""
2
+
3
+ import copy
4
+ import json
5
+
6
+
7
+ def substitute(value, variables):
8
+ """Replace {name} placeholders in every string of a fixture."""
9
+ if isinstance(value, str):
10
+ for name, text in variables.items():
11
+ value = value.replace("{" + name + "}", text)
12
+ return value
13
+ if isinstance(value, list):
14
+ return [substitute(item, variables) for item in value]
15
+ if isinstance(value, dict):
16
+ return {key: substitute(item, variables) for key, item in value.items()}
17
+ return value
18
+
19
+
20
+ def _merge(base, extra):
21
+ for key, value in (extra or {}).items():
22
+ if isinstance(value, dict) and isinstance(base.get(key), dict):
23
+ _merge(base[key], value)
24
+ else:
25
+ base[key] = copy.deepcopy(value)
26
+ return base
27
+
28
+
29
+ def _chat_tool(name, spec):
30
+ return {"type": "function", "function": {"name": name, "description": spec["description"], "parameters": spec["parameters"]}}
31
+
32
+
33
+ def _responses_tool(name, spec):
34
+ return {"type": "function", "name": name, "description": spec["description"], "parameters": spec["parameters"]}
35
+
36
+
37
+ def _chat_messages(turn):
38
+ messages = []
39
+ if turn.get("system"):
40
+ messages.append({"role": "system", "content": turn["system"]})
41
+ if turn.get("developer"):
42
+ messages.append({"role": "developer", "content": turn["developer"]})
43
+ for message in turn.get("messages", []):
44
+ if message["role"] == "assistant" and message.get("tool_calls"):
45
+ messages.append({
46
+ "role": "assistant",
47
+ "content": message.get("content"),
48
+ "tool_calls": [
49
+ {"id": c["id"], "type": "function", "function": {"name": c["name"], "arguments": c["arguments"]}}
50
+ for c in message["tool_calls"]
51
+ ],
52
+ })
53
+ elif message["role"] == "tool":
54
+ messages.append({"role": "tool", "tool_call_id": message["tool_call_id"], "content": message["content"]})
55
+ else:
56
+ messages.append({"role": message["role"], "content": message["content"]})
57
+ return messages
58
+
59
+
60
+ def _responses_input(turn):
61
+ items = []
62
+ if turn.get("developer"):
63
+ items.append({"role": "developer", "content": turn["developer"]})
64
+ for message in turn.get("messages", []):
65
+ if message["role"] == "assistant" and message.get("tool_calls"):
66
+ if message.get("content"):
67
+ items.append({"role": "assistant", "content": message["content"]})
68
+ for call in message["tool_calls"]:
69
+ items.append({"type": "function_call", "call_id": call["id"], "name": call["name"], "arguments": call["arguments"]})
70
+ elif message["role"] == "tool":
71
+ items.append({"type": "function_call_output", "call_id": message["tool_call_id"], "output": message["content"]})
72
+ else:
73
+ items.append({"role": message["role"], "content": message["content"]})
74
+ return items
75
+
76
+
77
+ def _tool_choice(protocol, choice):
78
+ if choice is None or isinstance(choice, str):
79
+ return choice
80
+ name = choice["name"]
81
+ if protocol == "chat":
82
+ return {"type": "function", "function": {"name": name}}
83
+ return {"type": "function", "name": name}
84
+
85
+
86
+ def build(protocol, model_id, turn, config, extra=None):
87
+ """Return the JSON body for one request on the given wire protocol."""
88
+ tools = {name: config["tools"][name] for name in turn.get("tools", [])}
89
+ limit_field = config["protocols"][protocol]["token_limit_field"]
90
+ body = {"model": model_id}
91
+ if protocol == "chat":
92
+ body["messages"] = _chat_messages(turn)
93
+ if tools:
94
+ body["tools"] = [_chat_tool(n, s) for n, s in tools.items()]
95
+ if turn.get("json_schema"):
96
+ spec = turn["json_schema"]
97
+ body["response_format"] = {
98
+ "type": "json_schema",
99
+ "json_schema": {"name": spec["name"], "schema": spec["schema"], "strict": True},
100
+ }
101
+ if turn.get("stream") and turn.get("include_usage"):
102
+ body["stream_options"] = {"include_usage": True}
103
+ if tools and "parallel_tool_calls" in turn:
104
+ body["parallel_tool_calls"] = turn["parallel_tool_calls"]
105
+ else:
106
+ if turn.get("system"):
107
+ body["instructions"] = turn["system"]
108
+ body["input"] = _responses_input(turn)
109
+ body["store"] = False
110
+ if tools:
111
+ body["tools"] = [_responses_tool(n, s) for n, s in tools.items()]
112
+ if turn.get("json_schema"):
113
+ spec = turn["json_schema"]
114
+ body["text"] = {"format": {"type": "json_schema", "name": spec["name"], "schema": spec["schema"], "strict": True}}
115
+ if tools and "parallel_tool_calls" in turn:
116
+ body["parallel_tool_calls"] = turn["parallel_tool_calls"]
117
+ if tools and turn.get("tool_choice") is not None:
118
+ body["tool_choice"] = _tool_choice(protocol, turn["tool_choice"])
119
+ if turn.get("max_tokens"):
120
+ body[limit_field] = turn["max_tokens"]
121
+ if turn.get("stream"):
122
+ body["stream"] = True
123
+ return _merge(body, extra)
124
+
125
+
126
+ def tool_schemas(config, names):
127
+ return {name: config["tools"][name]["parameters"] for name in names}
128
+
129
+
130
+ def dumps(body):
131
+ return json.dumps(body, separators=(",", ":")).encode()
@@ -0,0 +1,63 @@
1
+ """Minimal JSON Schema subset validator (no third-party dependency).
2
+
3
+ Supports the keywords used by tool and structured-output fixtures: type, enum,
4
+ const, properties, required, additionalProperties, items, minItems, maxItems,
5
+ minimum, maximum, minLength, maxLength and pattern.
6
+ """
7
+
8
+ import re
9
+
10
+ _TYPES = {
11
+ "object": lambda v: isinstance(v, dict),
12
+ "array": lambda v: isinstance(v, list),
13
+ "string": lambda v: isinstance(v, str),
14
+ "integer": lambda v: isinstance(v, int) and not isinstance(v, bool),
15
+ "number": lambda v: isinstance(v, (int, float)) and not isinstance(v, bool),
16
+ "boolean": lambda v: isinstance(v, bool),
17
+ "null": lambda v: v is None,
18
+ }
19
+
20
+
21
+ def errors(value, schema, path="$"):
22
+ """Return a list of 'path: problem' strings; empty means valid."""
23
+ found = []
24
+ expected = schema.get("type")
25
+ if expected is not None:
26
+ names = expected if isinstance(expected, list) else [expected]
27
+ if not any(_TYPES[name](value) for name in names):
28
+ return [path + ": expected " + "|".join(names)]
29
+ if "const" in schema and value != schema["const"]:
30
+ found.append(path + ": does not equal the required constant")
31
+ if "enum" in schema and value not in schema["enum"]:
32
+ found.append(path + ": not one of the allowed values")
33
+ if isinstance(value, str):
34
+ if len(value) < schema.get("minLength", 0):
35
+ found.append(path + ": shorter than minLength")
36
+ if len(value) > schema.get("maxLength", 1 << 30):
37
+ found.append(path + ": longer than maxLength")
38
+ if "pattern" in schema and not re.search(schema["pattern"], value):
39
+ found.append(path + ": does not match pattern")
40
+ if _TYPES["number"](value):
41
+ if "minimum" in schema and value < schema["minimum"]:
42
+ found.append(path + ": below minimum")
43
+ if "maximum" in schema and value > schema["maximum"]:
44
+ found.append(path + ": above maximum")
45
+ if isinstance(value, list):
46
+ if len(value) < schema.get("minItems", 0):
47
+ found.append(path + ": fewer than minItems")
48
+ if len(value) > schema.get("maxItems", 1 << 30):
49
+ found.append(path + ": more than maxItems")
50
+ if "items" in schema:
51
+ for index, item in enumerate(value):
52
+ found += errors(item, schema["items"], "%s[%d]" % (path, index))
53
+ if isinstance(value, dict):
54
+ properties = schema.get("properties", {})
55
+ for name in schema.get("required", []):
56
+ if name not in value:
57
+ found.append(path + "." + name + ": required")
58
+ for name, item in value.items():
59
+ if name in properties:
60
+ found += errors(item, properties[name], path + "." + name)
61
+ elif schema.get("additionalProperties") is False:
62
+ found.append(path + "." + name + ": unexpected property")
63
+ return found
@@ -0,0 +1,111 @@
1
+ """Run declarative routing, rate-limit and fault scenarios against the local emulator.
2
+
3
+ Evidence from these scenarios is labelled ``simulated``. It proves the harness
4
+ detects and classifies the behavior; it never proves the production proxy's
5
+ accounts, quotas or failover are healthy.
6
+ """
7
+
8
+ import random
9
+ import time
10
+
11
+ from . import checks, protocol
12
+ from .emulator import Account, Emulator
13
+ from .redact import Redactor
14
+ from .transport import Client
15
+
16
+ DEFAULT_TURN = {"messages": [{"role": "user", "content": "Reply with the single word OK."}], "max_tokens": 64}
17
+
18
+
19
+ def _expect(step_expect, outcomes, emulator, sent, elapsed):
20
+ """Return a list of mismatches for one step's expectations."""
21
+ problems = []
22
+ last = outcomes[-1]
23
+ wanted = step_expect
24
+ if "status" in wanted and any(o.status != wanted["status"] for o in outcomes):
25
+ problems.append("HTTP status %s, expected %s" % ([o.status for o in outcomes], wanted["status"]))
26
+ if "category" in wanted and any(o.category != wanted["category"] for o in outcomes):
27
+ problems.append("category %s, expected %s" % ([o.category for o in outcomes], wanted["category"]))
28
+ served = [o.headers.get("x-emulator-account") for o in outcomes]
29
+ if "served_by_all" in wanted and any(s not in wanted["served_by_all"] for s in served):
30
+ problems.append("served by %s, expected only %s" % (served, wanted["served_by_all"]))
31
+ if "served_sequence" in wanted and served != wanted["served_sequence"]:
32
+ problems.append("served by %s, expected the sequence %s" % (served, wanted["served_sequence"]))
33
+ if "served_set" in wanted and set(served) != set(wanted["served_set"]):
34
+ problems.append("served by %s, expected exactly %s" % (sorted(set(served)), wanted["served_set"]))
35
+ if wanted.get("single_account") and len(set(served)) != 1:
36
+ problems.append("session was not pinned to one account: %s" % sorted(set(served)))
37
+ if wanted.get("retry_after") and not last.retry_after:
38
+ problems.append("Retry-After header was not surfaced")
39
+ if wanted.get("envelope") and "no error envelope" in (last.detail or ""):
40
+ problems.append("error response lacks an OpenAI error envelope")
41
+ if wanted.get("no_retry") and emulator.requests != sent:
42
+ problems.append("harness sent %d requests where %d were issued (hidden retry)" % (emulator.requests, sent))
43
+ if "max_elapsed_s" in wanted and elapsed > wanted["max_elapsed_s"]:
44
+ problems.append("took %.1fs, limit %.1fs" % (elapsed, wanted["max_elapsed_s"]))
45
+ if wanted.get("single_model") and len({o.norm.model for o in outcomes if o.norm}) != 1:
46
+ problems.append("response model id differed across accounts")
47
+ if "violation" in wanted:
48
+ if not any(wanted["violation"] in v for o in outcomes if o.norm for v in o.norm.violations):
49
+ problems.append("expected protocol violation %r was not detected" % wanted["violation"])
50
+ elif wanted.get("status") == 200:
51
+ for o in outcomes:
52
+ if o.norm is None or o.norm.violations:
53
+ problems.append("successful response was not contract-valid")
54
+ break
55
+ return problems
56
+
57
+
58
+ def run_scenario(config, scenario, seed=0):
59
+ model_ids = [m["id"] for m in config.models]
60
+ accounts = []
61
+ for spec in scenario["accounts"]:
62
+ account = Account(spec["id"], quota=spec.get("quota"))
63
+ account.fail_next = spec.get("fail_next", 0)
64
+ accounts.append(account)
65
+ emulator = Emulator(model_ids, accounts=accounts, strategy=scenario.get("strategy", config.raw["routing"]["strategy"]))
66
+ base = emulator.start()
67
+ redact = Redactor([emulator.key])
68
+ timeouts = dict(config.raw["timeouts"], **scenario.get("timeouts", {}))
69
+ budget = checks.Budget(10_000, 10_000, 10_000_000)
70
+ ctx = checks.Context(config, config.models[0],
71
+ lambda model_id: Client(base, emulator.key, redact, timeouts["request_s"],
72
+ lambda kind: budget.take(model_id) if kind in ("inference", "probe") else None),
73
+ budget, random.Random(seed), redact, timeouts)
74
+ details, sent = [], 0
75
+ try:
76
+ for number, step in enumerate(scenario["steps"], 1):
77
+ action = step.get("action")
78
+ if action == "advance":
79
+ emulator.advance(step["seconds"])
80
+ elif action == "account":
81
+ account = emulator.account(step["id"])
82
+ account.quota = step.get("quota", account.quota)
83
+ account.fail_next = step.get("fail_next", account.fail_next)
84
+ elif action == "fault":
85
+ emulator.fault(**step["faults"])
86
+ elif action == "clear_faults":
87
+ emulator.clear_faults()
88
+ else:
89
+ request = step["request"]
90
+ turn = dict(request.get("turn") or DEFAULT_TURN)
91
+ outcomes, started = [], time.monotonic()
92
+ for index in range(step.get("count", 1)):
93
+ # Distinct sessions are new clients (cold bindings); "same" is one session (affinity).
94
+ session = "same" if request.get("session") == "same" else "s%d-%d" % (number, index)
95
+ outcomes.append(checks.call(ctx, request["protocol"], turn, {},
96
+ headers={"X-Session-Id": session if session != "same" else "pinned"}))
97
+ sent += 1
98
+ problems = _expect(step["expect"], outcomes, emulator, sent, time.monotonic() - started)
99
+ details += ["step %d: %s" % (number, p) for p in problems]
100
+ except Exception as error: # a scenario defect must be visible, not swallowed
101
+ details.append("harness error: %s" % type(error).__name__)
102
+ finally:
103
+ log = list(emulator.routing_log)
104
+ emulator.stop()
105
+ return {"id": scenario["id"], "capability": scenario["capability"], "title": scenario["title"],
106
+ "status": "fail" if details else "pass", "evidence": "simulated", "details": details,
107
+ "routing": [{"tried": entry["tried"], "served_by": entry["served_by"]} for entry in log]}
108
+
109
+
110
+ def run_all(config, seed=0):
111
+ return [run_scenario(config, scenario, seed) for scenario in config.raw.get("simulations", [])]
@@ -0,0 +1,213 @@
1
+ """HTTP/SSE client with strict framing checks, deadlines and no redirects or hidden retries."""
2
+
3
+ import http.client
4
+ import json
5
+ import ssl
6
+ import time
7
+ import urllib.parse
8
+
9
+ USER_AGENT = "cliproxy-fleet/1.0 model-health"
10
+ MAX_BODY = 4 * 1024 * 1024
11
+ _SSE_FIELDS = ("data", "event", "id", "retry")
12
+
13
+
14
+ class TransportError(Exception):
15
+ """A request that produced no HTTP response; category feeds failure reporting."""
16
+
17
+ def __init__(self, category, detail):
18
+ super().__init__(detail)
19
+ self.category = category
20
+ self.detail = detail
21
+
22
+
23
+ class Response:
24
+ def __init__(self, status, headers, body, elapsed):
25
+ self.status = status
26
+ self.headers = {k.lower(): v for k, v in headers}
27
+ self.body = body
28
+ self.elapsed = elapsed
29
+
30
+ def json(self):
31
+ try:
32
+ return json.loads(self.body.decode("utf-8"))
33
+ except (ValueError, UnicodeDecodeError):
34
+ return None
35
+
36
+
37
+ class Frame:
38
+ def __init__(self, event, data, at):
39
+ self.event = event
40
+ self.data = data
41
+ self.at = at # seconds since the request was sent
42
+
43
+
44
+ class StreamResult:
45
+ def __init__(self):
46
+ self.status = 0
47
+ self.headers = {}
48
+ self.frames = []
49
+ self.violations = []
50
+ self.error_body = b""
51
+ self.ttft = None # first frame carrying data
52
+ self.elapsed = 0.0
53
+ self.cancelled = False
54
+ self.done = False # saw the literal [DONE] sentinel
55
+ self.clean_eof = False # server ended the body after a complete frame
56
+
57
+ def json_frames(self):
58
+ out = []
59
+ for frame in self.frames:
60
+ if frame.data == "[DONE]":
61
+ continue
62
+ try:
63
+ out.append((frame.event, json.loads(frame.data)))
64
+ except ValueError:
65
+ self.violations.append("non-JSON data frame")
66
+ return out
67
+
68
+
69
+ def _loopback(host):
70
+ return host in ("127.0.0.1", "localhost", "::1")
71
+
72
+
73
+ class Client:
74
+ """One origin + optional bearer key. Every call is a single attempt."""
75
+
76
+ def __init__(self, base_url, key=None, redactor=None, timeout=60.0, on_request=None):
77
+ parts = urllib.parse.urlsplit(base_url)
78
+ if parts.scheme not in ("https", "http") or not parts.hostname:
79
+ raise ValueError("Endpoint must be an absolute URL")
80
+ if parts.scheme == "http" and not _loopback(parts.hostname):
81
+ raise ValueError("HTTPS required (plain HTTP is allowed only for loopback emulators)")
82
+ self.scheme = parts.scheme
83
+ self.host = parts.hostname
84
+ self.port = parts.port
85
+ self.prefix = parts.path.rstrip("/")
86
+ self.base_url = base_url.rstrip("/")
87
+ self.key = key
88
+ self.redact = redactor or (lambda text, limit=300: str(text)[:limit])
89
+ self.timeout = timeout
90
+ self.on_request = on_request or (lambda kind: None)
91
+
92
+ def _connect(self, timeout):
93
+ if self.scheme == "https":
94
+ return http.client.HTTPSConnection(
95
+ self.host, self.port, timeout=timeout, context=ssl.create_default_context()
96
+ )
97
+ return http.client.HTTPConnection(self.host, self.port, timeout=timeout)
98
+
99
+ def _headers(self, authenticated, body, extra):
100
+ headers = {"User-Agent": USER_AGENT, "Accept": "application/json"}
101
+ if authenticated and self.key:
102
+ headers["Authorization"] = "Bearer " + self.key
103
+ if body is not None:
104
+ headers["Content-Type"] = "application/json"
105
+ headers.update(extra or {})
106
+ return headers
107
+
108
+ def _send(self, method, path, body, authenticated, timeout, extra, kind):
109
+ self.on_request(kind)
110
+ connection = self._connect(timeout or self.timeout)
111
+ payload = None
112
+ if body is not None:
113
+ payload = body if isinstance(body, bytes) else json.dumps(body).encode()
114
+ started = time.monotonic()
115
+ try:
116
+ connection.request(
117
+ method, self.prefix + path, payload, self._headers(authenticated, payload, extra)
118
+ )
119
+ return connection, connection.getresponse(), started
120
+ except Exception as error:
121
+ connection.close()
122
+ raise self._classify(error) from None
123
+
124
+ def _classify(self, error):
125
+ text = self.redact(type(error).__name__ + ": " + str(error))
126
+ if isinstance(error, (TimeoutError,)) or "timed out" in str(error):
127
+ return TransportError("timeout", text)
128
+ return TransportError("transport", text)
129
+
130
+ def request(self, method, path, body=None, authenticated=True, timeout=None,
131
+ headers=None, kind="request"):
132
+ connection, response, started = self._send(
133
+ method, path, body, authenticated, timeout, headers, kind
134
+ )
135
+ try:
136
+ data = response.read(MAX_BODY + 1)
137
+ except Exception as error:
138
+ raise self._classify(error) from None
139
+ finally:
140
+ connection.close()
141
+ if len(data) > MAX_BODY:
142
+ raise TransportError("protocol", "Response body exceeds the harness limit")
143
+ return Response(response.status, response.getheaders(), data, time.monotonic() - started)
144
+
145
+ def stream(self, path, body, timeout=None, deadline=None, stop_after=None,
146
+ headers=None, kind="inference"):
147
+ """POST a streaming request and parse SSE frames.
148
+
149
+ timeout is the per-read idle limit; deadline bounds the whole stream.
150
+ stop_after closes the connection after N data frames (cancellation).
151
+ """
152
+ result = StreamResult()
153
+ extra = {"Accept": "text/event-stream"}
154
+ extra.update(headers or {})
155
+ connection, response, started = self._send(
156
+ "POST", path, body, True, timeout, extra, kind
157
+ )
158
+ result.status = response.status
159
+ result.headers = {k.lower(): v for k, v in response.getheaders()}
160
+ try:
161
+ if response.status != 200:
162
+ result.error_body = response.read(64 * 1024)
163
+ return result
164
+ if "text/event-stream" not in result.headers.get("content-type", ""):
165
+ result.violations.append("content-type is not text/event-stream")
166
+ event, data_lines, saw_field = None, [], False
167
+ limit = deadline or (self.timeout * 4)
168
+ while True:
169
+ if time.monotonic() - started > limit:
170
+ raise TransportError("timeout", "Stream exceeded its overall deadline")
171
+ try:
172
+ raw = response.readline()
173
+ except Exception as error:
174
+ raise self._classify(error) from None
175
+ if not raw:
176
+ result.clean_eof = not saw_field
177
+ if saw_field:
178
+ result.violations.append("stream ended inside an unterminated frame")
179
+ break
180
+ line = raw.decode("utf-8", "replace").rstrip("\r\n")
181
+ if line == "":
182
+ if saw_field:
183
+ at = time.monotonic() - started
184
+ frame = Frame(event, "\n".join(data_lines), at)
185
+ if data_lines:
186
+ result.frames.append(frame)
187
+ if result.ttft is None:
188
+ result.ttft = at
189
+ if frame.data == "[DONE]":
190
+ result.done = True
191
+ event, data_lines, saw_field = None, [], False
192
+ if stop_after and len(result.frames) >= stop_after:
193
+ result.cancelled = True
194
+ break
195
+ continue
196
+ if line.startswith(":"):
197
+ continue # comment / keep-alive
198
+ name, sep, value = line.partition(":")
199
+ if name not in _SSE_FIELDS or (not sep and name != "data"):
200
+ result.violations.append("invalid SSE line")
201
+ continue
202
+ value = value[1:] if value.startswith(" ") else value
203
+ saw_field = True
204
+ if name == "data":
205
+ data_lines.append(value)
206
+ elif name == "event":
207
+ event = value
208
+ if result.done and name == "data":
209
+ result.violations.append("data after [DONE]")
210
+ finally:
211
+ connection.close()
212
+ result.elapsed = time.monotonic() - started
213
+ return result
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env python3
2
+ import sys
3
+
4
+ sys.dont_write_bytecode = True
5
+
6
+ import pathlib as _pathlib
7
+
8
+ sys.path.insert(0, str(_pathlib.Path(__file__).resolve().parent))
9
+ import cliproxy_host
10
+
11
+ import datetime, json, pathlib, subprocess, sys, os
12
+
13
+ r = cliproxy_host.default_root().resolve()
14
+ issues = []
15
+ provenance = subprocess.run([str(r / "scripts/audit.py")], capture_output=True)
16
+ if provenance.returncode:
17
+ issues.append("Installed code differs from its committed-source manifest.")
18
+ p = subprocess.run([str(r / "scripts/ops"), "health"], capture_output=True)
19
+ if p.returncode:
20
+ issues.append("Application health/authentication failed; inspect sanitized logs.")
21
+ try:
22
+ import yaml
23
+
24
+ strategy = yaml.safe_load((r / "state/runtime/config.yaml").read_text())["routing"]["strategy"]
25
+ if strategy != "fill-first":
26
+ issues.append(
27
+ "Live routing strategy is not fill-first (drift from config.template.yaml); apply a reviewed config with `cliproxy apply`."
28
+ )
29
+ except Exception:
30
+ issues.append("Live routing strategy could not be read.")
31
+ f = r / "state/status/backup-success.json"
32
+ if not f.exists():
33
+ issues.append("No successful independent backup has been recorded.")
34
+ else:
35
+ try:
36
+ t = datetime.datetime.fromisoformat(json.loads(f.read_text())["time"])
37
+ if (datetime.datetime.now(datetime.timezone.utc) - t).total_seconds() > 45 * 60:
38
+ issues.append("Cloud checkpoint older than 45 minutes.")
39
+ except Exception:
40
+ issues.append("Invalid backup status record.")
41
+ if (r / "state/status/vault-failed").exists():
42
+ issues.append("Vault login/renewal failed; backups cannot fetch credentials.")
43
+ for marker in [
44
+ "backup-failed",
45
+ "backup-check-failed",
46
+ "backup-operation-failed",
47
+ "gc-failed",
48
+ ]:
49
+ if (r / "state/status" / marker).exists():
50
+ issues.append(marker + ": inspect failed operation.")
51
+ gc = r / "state/status/gc-success.json"
52
+ try:
53
+ age = datetime.datetime.now(
54
+ datetime.timezone.utc
55
+ ) - datetime.datetime.fromisoformat(json.loads(gc.read_text())["time"])
56
+ if age.total_seconds() > 9 * 86400:
57
+ issues.append("Garbage collection older than nine days.")
58
+ except Exception:
59
+ issues.append("No valid successful garbage collection record.")
60
+ f = r / "state/status/monitor-failed"
61
+ if issues:
62
+ f.write_text("\n".join(issues) + "\n")
63
+ if os.geteuid() == 0:
64
+ os.chown(f, 1000, 1000)
65
+ print(f.read_text())
66
+ sys.exit(1)
67
+ f.unlink(missing_ok=True)
68
+ print("Health and backup freshness passed.")