@synth-deploy/server 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 (317) hide show
  1. package/dist/agent/debrief-retention.d.ts +12 -0
  2. package/dist/agent/debrief-retention.d.ts.map +1 -0
  3. package/dist/agent/debrief-retention.js +27 -0
  4. package/dist/agent/debrief-retention.js.map +1 -0
  5. package/dist/agent/envoy-client.d.ts +216 -0
  6. package/dist/agent/envoy-client.d.ts.map +1 -0
  7. package/dist/agent/envoy-client.js +266 -0
  8. package/dist/agent/envoy-client.js.map +1 -0
  9. package/dist/agent/envoy-registry.d.ts +102 -0
  10. package/dist/agent/envoy-registry.d.ts.map +1 -0
  11. package/dist/agent/envoy-registry.js +319 -0
  12. package/dist/agent/envoy-registry.js.map +1 -0
  13. package/dist/agent/health-checker.d.ts +39 -0
  14. package/dist/agent/health-checker.d.ts.map +1 -0
  15. package/dist/agent/health-checker.js +49 -0
  16. package/dist/agent/health-checker.js.map +1 -0
  17. package/dist/agent/mcp-client-manager.d.ts +36 -0
  18. package/dist/agent/mcp-client-manager.d.ts.map +1 -0
  19. package/dist/agent/mcp-client-manager.js +106 -0
  20. package/dist/agent/mcp-client-manager.js.map +1 -0
  21. package/dist/agent/stale-deployment-detector.d.ts +15 -0
  22. package/dist/agent/stale-deployment-detector.d.ts.map +1 -0
  23. package/dist/agent/stale-deployment-detector.js +50 -0
  24. package/dist/agent/stale-deployment-detector.js.map +1 -0
  25. package/dist/agent/step-runner.d.ts +31 -0
  26. package/dist/agent/step-runner.d.ts.map +1 -0
  27. package/dist/agent/step-runner.js +80 -0
  28. package/dist/agent/step-runner.js.map +1 -0
  29. package/dist/agent/synth-agent.d.ts +168 -0
  30. package/dist/agent/synth-agent.d.ts.map +1 -0
  31. package/dist/agent/synth-agent.js +1195 -0
  32. package/dist/agent/synth-agent.js.map +1 -0
  33. package/dist/api/agent.d.ts +36 -0
  34. package/dist/api/agent.d.ts.map +1 -0
  35. package/dist/api/agent.js +867 -0
  36. package/dist/api/agent.js.map +1 -0
  37. package/dist/api/api-keys.d.ts +4 -0
  38. package/dist/api/api-keys.d.ts.map +1 -0
  39. package/dist/api/api-keys.js +118 -0
  40. package/dist/api/api-keys.js.map +1 -0
  41. package/dist/api/artifacts.d.ts +5 -0
  42. package/dist/api/artifacts.d.ts.map +1 -0
  43. package/dist/api/artifacts.js +142 -0
  44. package/dist/api/artifacts.js.map +1 -0
  45. package/dist/api/auth.d.ts +4 -0
  46. package/dist/api/auth.d.ts.map +1 -0
  47. package/dist/api/auth.js +280 -0
  48. package/dist/api/auth.js.map +1 -0
  49. package/dist/api/deployments.d.ts +11 -0
  50. package/dist/api/deployments.d.ts.map +1 -0
  51. package/dist/api/deployments.js +1098 -0
  52. package/dist/api/deployments.js.map +1 -0
  53. package/dist/api/environments.d.ts +5 -0
  54. package/dist/api/environments.d.ts.map +1 -0
  55. package/dist/api/environments.js +69 -0
  56. package/dist/api/environments.js.map +1 -0
  57. package/dist/api/envoy-reports.d.ts +17 -0
  58. package/dist/api/envoy-reports.d.ts.map +1 -0
  59. package/dist/api/envoy-reports.js +138 -0
  60. package/dist/api/envoy-reports.js.map +1 -0
  61. package/dist/api/envoys.d.ts +5 -0
  62. package/dist/api/envoys.d.ts.map +1 -0
  63. package/dist/api/envoys.js +192 -0
  64. package/dist/api/envoys.js.map +1 -0
  65. package/dist/api/fleet.d.ts +11 -0
  66. package/dist/api/fleet.d.ts.map +1 -0
  67. package/dist/api/fleet.js +394 -0
  68. package/dist/api/fleet.js.map +1 -0
  69. package/dist/api/graph.d.ts +8 -0
  70. package/dist/api/graph.d.ts.map +1 -0
  71. package/dist/api/graph.js +355 -0
  72. package/dist/api/graph.js.map +1 -0
  73. package/dist/api/health.d.ts +20 -0
  74. package/dist/api/health.d.ts.map +1 -0
  75. package/dist/api/health.js +248 -0
  76. package/dist/api/health.js.map +1 -0
  77. package/dist/api/idp-schemas.d.ts +41 -0
  78. package/dist/api/idp-schemas.d.ts.map +1 -0
  79. package/dist/api/idp-schemas.js +17 -0
  80. package/dist/api/idp-schemas.js.map +1 -0
  81. package/dist/api/idp.d.ts +6 -0
  82. package/dist/api/idp.d.ts.map +1 -0
  83. package/dist/api/idp.js +620 -0
  84. package/dist/api/idp.js.map +1 -0
  85. package/dist/api/intake.d.ts +10 -0
  86. package/dist/api/intake.d.ts.map +1 -0
  87. package/dist/api/intake.js +418 -0
  88. package/dist/api/intake.js.map +1 -0
  89. package/dist/api/partitions.d.ts +5 -0
  90. package/dist/api/partitions.d.ts.map +1 -0
  91. package/dist/api/partitions.js +113 -0
  92. package/dist/api/partitions.js.map +1 -0
  93. package/dist/api/progress-event-store.d.ts +62 -0
  94. package/dist/api/progress-event-store.d.ts.map +1 -0
  95. package/dist/api/progress-event-store.js +118 -0
  96. package/dist/api/progress-event-store.js.map +1 -0
  97. package/dist/api/schemas.d.ts +1000 -0
  98. package/dist/api/schemas.d.ts.map +1 -0
  99. package/dist/api/schemas.js +328 -0
  100. package/dist/api/schemas.js.map +1 -0
  101. package/dist/api/security-boundaries.d.ts +4 -0
  102. package/dist/api/security-boundaries.d.ts.map +1 -0
  103. package/dist/api/security-boundaries.js +32 -0
  104. package/dist/api/security-boundaries.js.map +1 -0
  105. package/dist/api/settings.d.ts +4 -0
  106. package/dist/api/settings.d.ts.map +1 -0
  107. package/dist/api/settings.js +99 -0
  108. package/dist/api/settings.js.map +1 -0
  109. package/dist/api/system.d.ts +75 -0
  110. package/dist/api/system.d.ts.map +1 -0
  111. package/dist/api/system.js +558 -0
  112. package/dist/api/system.js.map +1 -0
  113. package/dist/api/telemetry.d.ts +4 -0
  114. package/dist/api/telemetry.d.ts.map +1 -0
  115. package/dist/api/telemetry.js +24 -0
  116. package/dist/api/telemetry.js.map +1 -0
  117. package/dist/api/users.d.ts +4 -0
  118. package/dist/api/users.d.ts.map +1 -0
  119. package/dist/api/users.js +173 -0
  120. package/dist/api/users.js.map +1 -0
  121. package/dist/archive-unpacker.d.ts +24 -0
  122. package/dist/archive-unpacker.d.ts.map +1 -0
  123. package/dist/archive-unpacker.js +239 -0
  124. package/dist/archive-unpacker.js.map +1 -0
  125. package/dist/artifact-analyzer.d.ts +59 -0
  126. package/dist/artifact-analyzer.d.ts.map +1 -0
  127. package/dist/artifact-analyzer.js +334 -0
  128. package/dist/artifact-analyzer.js.map +1 -0
  129. package/dist/auth/idp/index.d.ts +9 -0
  130. package/dist/auth/idp/index.d.ts.map +1 -0
  131. package/dist/auth/idp/index.js +5 -0
  132. package/dist/auth/idp/index.js.map +1 -0
  133. package/dist/auth/idp/ldap.d.ts +56 -0
  134. package/dist/auth/idp/ldap.d.ts.map +1 -0
  135. package/dist/auth/idp/ldap.js +276 -0
  136. package/dist/auth/idp/ldap.js.map +1 -0
  137. package/dist/auth/idp/oidc.d.ts +27 -0
  138. package/dist/auth/idp/oidc.d.ts.map +1 -0
  139. package/dist/auth/idp/oidc.js +97 -0
  140. package/dist/auth/idp/oidc.js.map +1 -0
  141. package/dist/auth/idp/role-mapping.d.ts +9 -0
  142. package/dist/auth/idp/role-mapping.d.ts.map +1 -0
  143. package/dist/auth/idp/role-mapping.js +16 -0
  144. package/dist/auth/idp/role-mapping.js.map +1 -0
  145. package/dist/auth/idp/saml.d.ts +40 -0
  146. package/dist/auth/idp/saml.d.ts.map +1 -0
  147. package/dist/auth/idp/saml.js +117 -0
  148. package/dist/auth/idp/saml.js.map +1 -0
  149. package/dist/auth/idp/types.d.ts +23 -0
  150. package/dist/auth/idp/types.d.ts.map +1 -0
  151. package/dist/auth/idp/types.js +2 -0
  152. package/dist/auth/idp/types.js.map +1 -0
  153. package/dist/fleet/fleet-executor.d.ts +35 -0
  154. package/dist/fleet/fleet-executor.d.ts.map +1 -0
  155. package/dist/fleet/fleet-executor.js +228 -0
  156. package/dist/fleet/fleet-executor.js.map +1 -0
  157. package/dist/fleet/fleet-store.d.ts +13 -0
  158. package/dist/fleet/fleet-store.d.ts.map +1 -0
  159. package/dist/fleet/fleet-store.js +13 -0
  160. package/dist/fleet/fleet-store.js.map +1 -0
  161. package/dist/fleet/index.d.ts +5 -0
  162. package/dist/fleet/index.d.ts.map +1 -0
  163. package/dist/fleet/index.js +4 -0
  164. package/dist/fleet/index.js.map +1 -0
  165. package/dist/fleet/representative-selector.d.ts +15 -0
  166. package/dist/fleet/representative-selector.d.ts.map +1 -0
  167. package/dist/fleet/representative-selector.js +71 -0
  168. package/dist/fleet/representative-selector.js.map +1 -0
  169. package/dist/graph/graph-executor.d.ts +36 -0
  170. package/dist/graph/graph-executor.d.ts.map +1 -0
  171. package/dist/graph/graph-executor.js +348 -0
  172. package/dist/graph/graph-executor.js.map +1 -0
  173. package/dist/graph/graph-inference.d.ts +22 -0
  174. package/dist/graph/graph-inference.d.ts.map +1 -0
  175. package/dist/graph/graph-inference.js +149 -0
  176. package/dist/graph/graph-inference.js.map +1 -0
  177. package/dist/graph/graph-store.d.ts +12 -0
  178. package/dist/graph/graph-store.d.ts.map +1 -0
  179. package/dist/graph/graph-store.js +61 -0
  180. package/dist/graph/graph-store.js.map +1 -0
  181. package/dist/graph/index.d.ts +5 -0
  182. package/dist/graph/index.d.ts.map +1 -0
  183. package/dist/graph/index.js +4 -0
  184. package/dist/graph/index.js.map +1 -0
  185. package/dist/index.d.ts +2 -0
  186. package/dist/index.d.ts.map +1 -0
  187. package/dist/index.js +837 -0
  188. package/dist/index.js.map +1 -0
  189. package/dist/intake/index.d.ts +6 -0
  190. package/dist/intake/index.d.ts.map +1 -0
  191. package/dist/intake/index.js +5 -0
  192. package/dist/intake/index.js.map +1 -0
  193. package/dist/intake/intake-processor.d.ts +17 -0
  194. package/dist/intake/intake-processor.d.ts.map +1 -0
  195. package/dist/intake/intake-processor.js +99 -0
  196. package/dist/intake/intake-processor.js.map +1 -0
  197. package/dist/intake/intake-store.d.ts +7 -0
  198. package/dist/intake/intake-store.d.ts.map +1 -0
  199. package/dist/intake/intake-store.js +7 -0
  200. package/dist/intake/intake-store.js.map +1 -0
  201. package/dist/intake/registry-poller.d.ts +41 -0
  202. package/dist/intake/registry-poller.d.ts.map +1 -0
  203. package/dist/intake/registry-poller.js +202 -0
  204. package/dist/intake/registry-poller.js.map +1 -0
  205. package/dist/intake/webhook-handlers.d.ts +37 -0
  206. package/dist/intake/webhook-handlers.d.ts.map +1 -0
  207. package/dist/intake/webhook-handlers.js +268 -0
  208. package/dist/intake/webhook-handlers.js.map +1 -0
  209. package/dist/logger.d.ts +5 -0
  210. package/dist/logger.d.ts.map +1 -0
  211. package/dist/logger.js +15 -0
  212. package/dist/logger.js.map +1 -0
  213. package/dist/mcp/resources.d.ts +9 -0
  214. package/dist/mcp/resources.d.ts.map +1 -0
  215. package/dist/mcp/resources.js +72 -0
  216. package/dist/mcp/resources.js.map +1 -0
  217. package/dist/mcp/server.d.ts +15 -0
  218. package/dist/mcp/server.d.ts.map +1 -0
  219. package/dist/mcp/server.js +20 -0
  220. package/dist/mcp/server.js.map +1 -0
  221. package/dist/mcp/tools.d.ts +9 -0
  222. package/dist/mcp/tools.d.ts.map +1 -0
  223. package/dist/mcp/tools.js +88 -0
  224. package/dist/mcp/tools.js.map +1 -0
  225. package/dist/middleware/auth.d.ts +29 -0
  226. package/dist/middleware/auth.d.ts.map +1 -0
  227. package/dist/middleware/auth.js +76 -0
  228. package/dist/middleware/auth.js.map +1 -0
  229. package/dist/middleware/permissions.d.ts +13 -0
  230. package/dist/middleware/permissions.d.ts.map +1 -0
  231. package/dist/middleware/permissions.js +32 -0
  232. package/dist/middleware/permissions.js.map +1 -0
  233. package/dist/pattern-store.d.ts +104 -0
  234. package/dist/pattern-store.d.ts.map +1 -0
  235. package/dist/pattern-store.js +299 -0
  236. package/dist/pattern-store.js.map +1 -0
  237. package/package.json +54 -0
  238. package/src/agent/debrief-retention.ts +44 -0
  239. package/src/agent/envoy-client.ts +474 -0
  240. package/src/agent/envoy-registry.ts +384 -0
  241. package/src/agent/health-checker.ts +70 -0
  242. package/src/agent/mcp-client-manager.ts +131 -0
  243. package/src/agent/stale-deployment-detector.ts +79 -0
  244. package/src/agent/step-runner.ts +124 -0
  245. package/src/agent/synth-agent.ts +1567 -0
  246. package/src/api/agent.ts +1075 -0
  247. package/src/api/api-keys.ts +129 -0
  248. package/src/api/artifacts.ts +194 -0
  249. package/src/api/auth.ts +320 -0
  250. package/src/api/deployments.ts +1347 -0
  251. package/src/api/environments.ts +97 -0
  252. package/src/api/envoy-reports.ts +159 -0
  253. package/src/api/envoys.ts +237 -0
  254. package/src/api/fleet.ts +510 -0
  255. package/src/api/graph.ts +516 -0
  256. package/src/api/health.ts +311 -0
  257. package/src/api/idp-schemas.ts +19 -0
  258. package/src/api/idp.ts +735 -0
  259. package/src/api/intake.ts +537 -0
  260. package/src/api/partitions.ts +147 -0
  261. package/src/api/progress-event-store.ts +153 -0
  262. package/src/api/schemas.ts +376 -0
  263. package/src/api/security-boundaries.ts +54 -0
  264. package/src/api/settings.ts +118 -0
  265. package/src/api/system.ts +704 -0
  266. package/src/api/telemetry.ts +32 -0
  267. package/src/api/users.ts +210 -0
  268. package/src/archive-unpacker.ts +271 -0
  269. package/src/artifact-analyzer.ts +438 -0
  270. package/src/auth/idp/index.ts +8 -0
  271. package/src/auth/idp/ldap.ts +340 -0
  272. package/src/auth/idp/oidc.ts +117 -0
  273. package/src/auth/idp/role-mapping.ts +22 -0
  274. package/src/auth/idp/saml.ts +148 -0
  275. package/src/auth/idp/types.ts +22 -0
  276. package/src/fleet/fleet-executor.ts +309 -0
  277. package/src/fleet/fleet-store.ts +13 -0
  278. package/src/fleet/index.ts +4 -0
  279. package/src/fleet/representative-selector.ts +83 -0
  280. package/src/graph/graph-executor.ts +446 -0
  281. package/src/graph/graph-inference.ts +184 -0
  282. package/src/graph/graph-store.ts +75 -0
  283. package/src/graph/index.ts +4 -0
  284. package/src/index.ts +916 -0
  285. package/src/intake/index.ts +5 -0
  286. package/src/intake/intake-processor.ts +111 -0
  287. package/src/intake/intake-store.ts +7 -0
  288. package/src/intake/registry-poller.ts +230 -0
  289. package/src/intake/webhook-handlers.ts +328 -0
  290. package/src/logger.ts +19 -0
  291. package/src/mcp/resources.ts +98 -0
  292. package/src/mcp/server.ts +34 -0
  293. package/src/mcp/tools.ts +117 -0
  294. package/src/middleware/auth.ts +103 -0
  295. package/src/middleware/permissions.ts +35 -0
  296. package/src/pattern-store.ts +409 -0
  297. package/tests/agent-mode.test.ts +536 -0
  298. package/tests/api-handlers.test.ts +1245 -0
  299. package/tests/archive-unpacker.test.ts +179 -0
  300. package/tests/artifact-analyzer.test.ts +240 -0
  301. package/tests/auth-middleware.test.ts +189 -0
  302. package/tests/decision-diary.test.ts +957 -0
  303. package/tests/diary-reader.test.ts +782 -0
  304. package/tests/envoy-client.test.ts +342 -0
  305. package/tests/envoy-reports.test.ts +156 -0
  306. package/tests/mcp-tools.test.ts +213 -0
  307. package/tests/orchestration.test.ts +536 -0
  308. package/tests/partition-deletion.test.ts +143 -0
  309. package/tests/partition-isolation.test.ts +830 -0
  310. package/tests/pattern-store.test.ts +371 -0
  311. package/tests/rbac-enforcement.test.ts +409 -0
  312. package/tests/ssrf-validation.test.ts +56 -0
  313. package/tests/stale-deployment.test.ts +85 -0
  314. package/tests/step-runner.test.ts +308 -0
  315. package/tests/ui-journey.test.ts +330 -0
  316. package/tsconfig.json +11 -0
  317. package/vitest.config.ts +27 -0
@@ -0,0 +1,311 @@
1
+ import fs from "node:fs";
2
+ import type { FastifyInstance } from "fastify";
3
+ import type Database from "better-sqlite3";
4
+ import type { LlmClient } from "@synth-deploy/core";
5
+ import { VerifyTaskModelSchema } from "./schemas.js";
6
+
7
+ interface HealthCheckOptions {
8
+ entityDb: Database.Database;
9
+ dataDir: string;
10
+ envoyUrl?: string;
11
+ llmApiKey?: string;
12
+ llmBaseUrl?: string;
13
+ mcpServers?: Array<{ name: string; url: string }>;
14
+ llmClient?: LlmClient;
15
+ }
16
+
17
+ interface CheckResult {
18
+ ok: boolean;
19
+ error?: string;
20
+ responseTimeMs?: number;
21
+ }
22
+
23
+ // Cached LLM health result (5-minute TTL)
24
+ let llmHealthCache: { healthy: boolean; checkedAt: number } | null = null;
25
+ const LLM_HEALTH_CACHE_TTL_MS = 300_000;
26
+
27
+ /** Call when the API key changes so the next health check runs fresh. */
28
+ export function invalidateLlmHealthCache(): void {
29
+ llmHealthCache = null;
30
+ }
31
+
32
+ export function registerHealthRoutes(
33
+ app: FastifyInstance,
34
+ options?: HealthCheckOptions,
35
+ ): void {
36
+ app.get("/health", async () => {
37
+ if (!options) {
38
+ return { status: "ok", service: "synth-server", timestamp: new Date().toISOString() };
39
+ }
40
+
41
+ const checks: Record<string, CheckResult> = {};
42
+
43
+ // SQLite check
44
+ try {
45
+ const start = Date.now();
46
+ options.entityDb.prepare("SELECT 1").get();
47
+ checks.db = { ok: true, responseTimeMs: Date.now() - start };
48
+ } catch (err) {
49
+ app.log.error(err, "Health check: database unavailable");
50
+ checks.db = { ok: false, error: "Database unavailable" };
51
+ }
52
+
53
+ // Filesystem check
54
+ try {
55
+ fs.accessSync(options.dataDir, fs.constants.R_OK | fs.constants.W_OK);
56
+ checks.fs = { ok: true };
57
+ } catch (err) {
58
+ app.log.error(err, "Health check: filesystem unavailable");
59
+ checks.fs = { ok: false, error: "Filesystem unavailable" };
60
+ }
61
+
62
+ // Envoy check (optional)
63
+ if (options.envoyUrl) {
64
+ try {
65
+ const start = Date.now();
66
+ const controller = new AbortController();
67
+ const timeout = setTimeout(() => controller.abort(), 3000);
68
+ const res = await fetch(`${options.envoyUrl}/health`, { signal: controller.signal });
69
+ clearTimeout(timeout);
70
+ checks.envoy = {
71
+ ok: res.ok,
72
+ responseTimeMs: Date.now() - start,
73
+ ...(res.ok ? {} : { error: `HTTP ${res.status}` }),
74
+ };
75
+ } catch (err) {
76
+ app.log.error(err, "Health check: envoy unreachable");
77
+ checks.envoy = { ok: false, error: "Envoy unreachable" };
78
+ }
79
+ }
80
+
81
+ // LLM API check (optional)
82
+ if (options.llmApiKey) {
83
+ try {
84
+ const start = Date.now();
85
+ const controller = new AbortController();
86
+ const timeout = setTimeout(() => controller.abort(), 5000);
87
+ const baseUrl = options.llmBaseUrl ?? "https://api.anthropic.com";
88
+ const res = await fetch(`${baseUrl}/v1/messages`, {
89
+ method: "POST",
90
+ headers: {
91
+ "x-api-key": options.llmApiKey,
92
+ "anthropic-version": "2023-06-01",
93
+ "content-type": "application/json",
94
+ },
95
+ body: JSON.stringify({
96
+ model: "claude-haiku-4-5-20251001",
97
+ max_tokens: 1,
98
+ messages: [{ role: "user", content: "health check" }],
99
+ }),
100
+ signal: controller.signal,
101
+ });
102
+ clearTimeout(timeout);
103
+ // Any non-5xx response means the API is reachable and the key is valid
104
+ checks.llm = {
105
+ ok: res.status < 500,
106
+ responseTimeMs: Date.now() - start,
107
+ ...(!res.ok && res.status >= 500 ? { error: `HTTP ${res.status}` } : {}),
108
+ };
109
+ } catch (err) {
110
+ app.log.error(err, "Health check: LLM API unavailable");
111
+ checks.llm = { ok: false, error: "LLM API unavailable" };
112
+ }
113
+ }
114
+
115
+ // MCP server checks (optional)
116
+ if (options.mcpServers && options.mcpServers.length > 0) {
117
+ const mcpResults: Record<string, CheckResult> = {};
118
+ for (const server of options.mcpServers) {
119
+ try {
120
+ const start = Date.now();
121
+ const controller = new AbortController();
122
+ const timeout = setTimeout(() => controller.abort(), 3000);
123
+ const res = await fetch(server.url, { signal: controller.signal });
124
+ clearTimeout(timeout);
125
+ mcpResults[server.name] = {
126
+ ok: res.ok,
127
+ responseTimeMs: Date.now() - start,
128
+ ...(!res.ok ? { error: `HTTP ${res.status}` } : {}),
129
+ };
130
+ } catch (err) {
131
+ app.log.error(err, `Health check: MCP server "${server.name}" unreachable`);
132
+ mcpResults[server.name] = { ok: false, error: "Unreachable" };
133
+ }
134
+ }
135
+ checks.mcpServers = mcpResults as unknown as CheckResult;
136
+ }
137
+
138
+ // Determine overall status
139
+ const dbOrFsFailed = !checks.db.ok || !checks.fs.ok;
140
+ const envoyFailed = checks.envoy && !checks.envoy.ok;
141
+ const llmFailed = checks.llm && !checks.llm.ok;
142
+ const mcpFailed = checks.mcpServers && Object.values(checks.mcpServers as unknown as Record<string, CheckResult>).some((c) => !c.ok);
143
+
144
+ let status: "healthy" | "degraded" | "unhealthy";
145
+ if (dbOrFsFailed) {
146
+ status = "unhealthy";
147
+ } else if (envoyFailed || llmFailed || mcpFailed) {
148
+ status = "degraded";
149
+ } else {
150
+ status = "healthy";
151
+ }
152
+
153
+ return {
154
+ status,
155
+ service: "synth-server",
156
+ checks,
157
+ timestamp: new Date().toISOString(),
158
+ };
159
+ });
160
+
161
+ // --- Dedicated LLM health endpoint ---
162
+ // Used by the UI's LlmGate to determine if the LLM connection is configured and reachable.
163
+
164
+ app.get("/api/health/llm", async () => {
165
+ const llmClient = options?.llmClient;
166
+
167
+ // Check configuration via the LlmClient instance
168
+ const configured = llmClient ? llmClient.isAvailable() : false;
169
+
170
+ if (!configured) {
171
+ return {
172
+ configured: false,
173
+ healthy: false,
174
+ provider: detectProvider(),
175
+ };
176
+ }
177
+
178
+ // Check cached health result
179
+ const now = Date.now();
180
+ if (llmHealthCache && now - llmHealthCache.checkedAt < LLM_HEALTH_CACHE_TTL_MS) {
181
+ return {
182
+ configured: true,
183
+ healthy: llmHealthCache.healthy,
184
+ provider: detectProvider(),
185
+ };
186
+ }
187
+
188
+ // For Anthropic/Bedrock/Vertex: trust configuration — real failures surface in the debrief.
189
+ // For openai-compatible: do a free GET /models to verify the endpoint is reachable.
190
+ let healthy = true;
191
+ try {
192
+ const apiKey = options?.llmApiKey ?? process.env.SYNTH_LLM_API_KEY;
193
+ const baseUrl = options?.llmBaseUrl ?? process.env.SYNTH_LLM_BASE_URL ?? "https://api.anthropic.com";
194
+ const provider = detectProvider();
195
+
196
+ if (provider === "openai-compatible") {
197
+ const controller = new AbortController();
198
+ const timeout = setTimeout(() => controller.abort(), 5000);
199
+ const res = await fetch(baseUrl.replace(/\/+$/, "") + "/models", {
200
+ signal: controller.signal,
201
+ headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
202
+ });
203
+ clearTimeout(timeout);
204
+ healthy = res.status < 500;
205
+ }
206
+ } catch (err) {
207
+ app.log.error(err, "LLM health check failed");
208
+ healthy = false;
209
+ }
210
+
211
+ llmHealthCache = { healthy, checkedAt: now };
212
+
213
+ return {
214
+ configured: true,
215
+ healthy,
216
+ provider: detectProvider(),
217
+ };
218
+ });
219
+
220
+ // --- Per-task model capability verification ---
221
+ // Sends a lightweight probe prompt to verify a model can handle a specific task.
222
+
223
+ app.post("/api/health/llm/verify-task", async (request, reply) => {
224
+ const llmClient = options?.llmClient;
225
+ if (!llmClient || !llmClient.isAvailable()) {
226
+ return reply.status(503).send({
227
+ error: "LLM client not configured or unavailable",
228
+ });
229
+ }
230
+
231
+ const parsed = VerifyTaskModelSchema.safeParse(request.body);
232
+ if (!parsed.success) {
233
+ return reply.status(400).send({
234
+ error: "Invalid input",
235
+ details: parsed.error.format(),
236
+ });
237
+ }
238
+
239
+ const { task, model } = parsed.data;
240
+
241
+ try {
242
+ const result = await runTaskModelVerification(llmClient, task, model);
243
+ return { result };
244
+ } catch (err) {
245
+ app.log.error(err, "Task model verification failed");
246
+ return reply.status(500).send({
247
+ error: "Verification failed",
248
+ detail: err instanceof Error ? err.message : String(err),
249
+ });
250
+ }
251
+ });
252
+ }
253
+
254
+ // ---------------------------------------------------------------------------
255
+ // Inline task model verification — avoids import issues in worktree builds.
256
+ // The canonical implementation lives in @synth-deploy/core (verifyModelCapability).
257
+ // ---------------------------------------------------------------------------
258
+
259
+ type TaskModelTask = "logClassification" | "diagnosticSynthesis" | "postmortemGeneration" | "queryAnswering";
260
+ // NOTE: "verified" = reachable, "insufficient" = unreachable.
261
+ // "marginal" is unused pending real capability probes (see issue #211).
262
+ type VerifyStatus = "verified" | "marginal" | "insufficient";
263
+
264
+ function extractApiErrorMessage(raw: string): string {
265
+ const match = raw.match(/"message"\s*:\s*"([^"]+)"/);
266
+ if (match) return match[1];
267
+ const stripped = raw.replace(/\{[^}]{20,}\}/g, "").trim();
268
+ return stripped || raw;
269
+ }
270
+
271
+ interface CapabilityVerificationResult {
272
+ task: string;
273
+ model: string;
274
+ status: VerifyStatus;
275
+ explanation: string;
276
+ }
277
+
278
+ async function runTaskModelVerification(
279
+ client: LlmClient,
280
+ task: string,
281
+ model: string,
282
+ ): Promise<CapabilityVerificationResult> {
283
+ // Connection test only — sends a minimal ping and checks for a response.
284
+ // Real capability evaluation is tracked in issue #211.
285
+ const result = await client.classify({
286
+ prompt: "ping",
287
+ systemPrompt: "Reply with the single word: pong",
288
+ promptSummary: `Connection test for ${task}`,
289
+ maxTokens: 16,
290
+ });
291
+
292
+ if (!result.ok) {
293
+ return {
294
+ task,
295
+ model,
296
+ status: "insufficient",
297
+ explanation: `Model could not be reached: ${extractApiErrorMessage(result.reason)}`,
298
+ };
299
+ }
300
+
301
+ return {
302
+ task,
303
+ model,
304
+ status: "verified",
305
+ explanation: `Model is reachable and responding.`,
306
+ };
307
+ }
308
+
309
+ function detectProvider(): string | undefined {
310
+ return process.env.SYNTH_LLM_PROVIDER ?? "anthropic";
311
+ }
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+
3
+ export const CreateIdpProviderSchema = z.object({
4
+ type: z.enum(["oidc", "saml", "ldap"]),
5
+ name: z.string().min(1),
6
+ enabled: z.boolean().optional(),
7
+ config: z.record(z.unknown()),
8
+ });
9
+
10
+ export const UpdateIdpProviderSchema = z.object({
11
+ name: z.string().min(1).optional(),
12
+ enabled: z.boolean().optional(),
13
+ config: z.record(z.unknown()).optional(),
14
+ });
15
+
16
+ export const CreateRoleMappingSchema = z.object({
17
+ idpGroup: z.string().min(1),
18
+ synthRole: z.string().min(1),
19
+ });