@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,97 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import type { IEnvironmentStore, IArtifactStore, ITelemetryStore } from "@synth-deploy/core";
3
+ import { CreateEnvironmentSchema, UpdateEnvironmentSchema } from "./schemas.js";
4
+ import { requirePermission } from "../middleware/permissions.js";
5
+ import type { DeploymentStore } from "../agent/synth-agent.js";
6
+
7
+ export function registerEnvironmentRoutes(
8
+ app: FastifyInstance,
9
+ environments: IEnvironmentStore,
10
+ deployments: DeploymentStore,
11
+ telemetry: ITelemetryStore,
12
+ ): void {
13
+ // List all environments
14
+ app.get("/api/environments", { preHandler: [requirePermission("environment.view")] }, async () => {
15
+ return { environments: environments.list() };
16
+ });
17
+
18
+ // Create an environment
19
+ app.post("/api/environments", { preHandler: [requirePermission("environment.create")] }, async (request, reply) => {
20
+ const parsed = CreateEnvironmentSchema.safeParse(request.body);
21
+ if (!parsed.success) {
22
+ return reply.status(400).send({ error: "Invalid input", details: parsed.error.format() });
23
+ }
24
+
25
+ const environment = environments.create(parsed.data.name.trim(), parsed.data.variables ?? {});
26
+ telemetry.record({ actor: (request.user?.email) ?? "anonymous", action: "environment.created", target: { type: "environment", id: environment.id }, details: { name: parsed.data.name } });
27
+ return reply.status(201).send({ environment });
28
+ });
29
+
30
+ // Get environment by ID
31
+ app.get<{ Params: { id: string } }>(
32
+ "/api/environments/:id",
33
+ { preHandler: [requirePermission("environment.view")] },
34
+ async (request, reply) => {
35
+ const environment = environments.get(request.params.id);
36
+ if (!environment) {
37
+ return reply.status(404).send({ error: "Environment not found" });
38
+ }
39
+ return { environment };
40
+ },
41
+ );
42
+
43
+ // Update environment
44
+ app.put<{ Params: { id: string } }>(
45
+ "/api/environments/:id",
46
+ { preHandler: [requirePermission("environment.update")] },
47
+ async (request, reply) => {
48
+ const parsed = UpdateEnvironmentSchema.safeParse(request.body);
49
+ if (!parsed.success) {
50
+ return reply.status(400).send({ error: "Invalid input", details: parsed.error.format() });
51
+ }
52
+
53
+ try {
54
+ const environment = environments.update(request.params.id, {
55
+ name: parsed.data.name?.trim(),
56
+ variables: parsed.data.variables,
57
+ });
58
+ telemetry.record({ actor: (request.user?.email) ?? "anonymous", action: "environment.updated", target: { type: "environment", id: request.params.id }, details: { name: parsed.data.name } });
59
+ return { environment };
60
+ } catch (err) {
61
+ if (err instanceof Error && err.message.toLowerCase().includes("not found")) {
62
+ return reply.status(404).send({ error: "Environment not found" });
63
+ }
64
+ app.log.error(err, "Failed to update environment");
65
+ return reply.status(500).send({ error: "Internal server error" });
66
+ }
67
+ },
68
+ );
69
+
70
+ // Delete environment (with linked-operations safety check)
71
+ app.delete<{ Params: { id: string } }>(
72
+ "/api/environments/:id",
73
+ { preHandler: [requirePermission("environment.delete")] },
74
+ async (request, reply) => {
75
+ const envId = request.params.id;
76
+ const env = environments.get(envId);
77
+ if (!env) {
78
+ return reply.status(404).send({ error: "Environment not found" });
79
+ }
80
+
81
+ // Check if any deployments reference this environment
82
+ const linkedDeployments = deployments
83
+ .list()
84
+ .filter((d) => d.environmentId === envId);
85
+
86
+ if (linkedDeployments.length > 0) {
87
+ return reply.status(409).send({
88
+ error: `Environment has ${linkedDeployments.length} deployment(s). Cannot delete an environment with deployment history.`,
89
+ deploymentCount: linkedDeployments.length,
90
+ });
91
+ }
92
+
93
+ environments.delete(envId);
94
+ return { deleted: true };
95
+ },
96
+ );
97
+ }
@@ -0,0 +1,159 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import { z } from "zod";
3
+ import { DecisionTypeEnum } from "@synth-deploy/core";
4
+ import type { DebriefWriter, DecisionType } from "@synth-deploy/core";
5
+ import type { DeploymentStore } from "../agent/synth-agent.js";
6
+ import type { EnvoyRegistry } from "../agent/envoy-registry.js";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Schema — validates incoming Envoy reports
10
+ // ---------------------------------------------------------------------------
11
+
12
+ const DebriefEntrySchema = z.object({
13
+ id: z.string(),
14
+ timestamp: z.string(),
15
+ partitionId: z.string().nullable(),
16
+ deploymentId: z.string().nullable(),
17
+ agent: z.enum(["server", "envoy"]),
18
+ decisionType: DecisionTypeEnum,
19
+ decision: z.string(),
20
+ reasoning: z.string(),
21
+ context: z.record(z.unknown()),
22
+ });
23
+
24
+ const EnvoyReportSchema = z.object({
25
+ type: z.literal("deployment-result"),
26
+ envoyId: z.string(),
27
+ deploymentId: z.string(),
28
+ success: z.boolean(),
29
+ failureReason: z.string().nullable(),
30
+ debriefEntries: z.array(DebriefEntrySchema),
31
+ summary: z.object({
32
+ artifacts: z.array(z.string()),
33
+ workspacePath: z.string(),
34
+ executionDurationMs: z.number(),
35
+ totalDurationMs: z.number(),
36
+ verificationPassed: z.boolean(),
37
+ verificationChecks: z.array(
38
+ z.object({
39
+ name: z.string(),
40
+ passed: z.boolean(),
41
+ detail: z.string(),
42
+ }),
43
+ ),
44
+ }),
45
+ });
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Route registration
49
+ // ---------------------------------------------------------------------------
50
+
51
+ /**
52
+ * Endpoint for Envoys to push reports back to Command.
53
+ *
54
+ * When a Envoy completes a deployment (success or failure), it pushes
55
+ * a report containing its full debrief entries. Command ingests these
56
+ * into its own debrief so there is one unified Debrief that contains
57
+ * both Command's orchestration decisions and the Envoy's execution
58
+ * decisions.
59
+ *
60
+ * This is the Envoy->Command direction of bidirectional communication.
61
+ */
62
+ export function registerEnvoyReportRoutes(
63
+ app: FastifyInstance,
64
+ debrief: DebriefWriter,
65
+ deployments: DeploymentStore,
66
+ registry: EnvoyRegistry,
67
+ ): void {
68
+ app.post("/api/envoy/report", async (request, reply) => {
69
+ const authHeader = (request.headers.authorization ?? "") as string;
70
+ const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : null;
71
+ if (!token || !registry.validateToken(token)) {
72
+ return reply.status(401).send({ error: "Invalid or missing envoy token" });
73
+ }
74
+
75
+ const parsed = EnvoyReportSchema.safeParse(request.body);
76
+ if (!parsed.success) {
77
+ return reply.status(400).send({
78
+ error: "Invalid Envoy report",
79
+ details: parsed.error.format(),
80
+ });
81
+ }
82
+
83
+ const report = parsed.data;
84
+
85
+ // Validate partition boundary: each debrief entry's deploymentId must
86
+ // belong to the claimed partitionId. Reject cross-partition reports.
87
+ for (const entry of report.debriefEntries) {
88
+ if (entry.deploymentId && entry.partitionId) {
89
+ const deployment = deployments.get(entry.deploymentId);
90
+ if (!deployment || deployment.partitionId !== entry.partitionId) {
91
+ debrief.record({
92
+ partitionId: entry.partitionId,
93
+ deploymentId: entry.deploymentId,
94
+ agent: "server",
95
+ decisionType: "system",
96
+ decision: "Rejected Envoy report: partition boundary violation",
97
+ reasoning: `Deployment ${entry.deploymentId} does not belong to partition ${entry.partitionId}. Report from envoy ${report.envoyId} rejected.`,
98
+ context: { envoyId: report.envoyId, reportedPartitionId: entry.partitionId },
99
+ });
100
+ return reply.status(403).send({
101
+ error: "Partition boundary violation",
102
+ detail: `Deployment ${entry.deploymentId} does not belong to partition ${entry.partitionId}`,
103
+ });
104
+ }
105
+ }
106
+ }
107
+
108
+ let ingested = 0;
109
+
110
+ // Ingest each Envoy debrief entry into Command's debrief.
111
+ // We re-record them (rather than inserting raw) so Command's debrief
112
+ // assigns its own IDs and timestamps. The original Envoy entry data
113
+ // is preserved in the context field for traceability.
114
+ for (const entry of report.debriefEntries) {
115
+ debrief.record({
116
+ partitionId: entry.partitionId,
117
+ deploymentId: entry.deploymentId,
118
+ agent: entry.agent as "server" | "envoy",
119
+ decisionType: entry.decisionType as DecisionType,
120
+ decision: entry.decision,
121
+ reasoning: entry.reasoning,
122
+ context: {
123
+ ...entry.context,
124
+ _envoyReport: {
125
+ envoyId: report.envoyId,
126
+ originalEntryId: entry.id,
127
+ originalTimestamp: entry.timestamp,
128
+ },
129
+ },
130
+ });
131
+ ingested++;
132
+ }
133
+
134
+ // Update deployment status based on the result
135
+ const deployment = deployments.get(report.deploymentId);
136
+ if (deployment && deployment.status === "running") {
137
+ let finalStatus: typeof deployment.status;
138
+ if (report.success) {
139
+ finalStatus = "succeeded" as typeof deployment.status;
140
+ } else {
141
+ const hadRollback = report.debriefEntries.some((e) => e.decisionType === "rollback-execution");
142
+ finalStatus = (hadRollback ? "rolled_back" : "failed") as typeof deployment.status;
143
+ }
144
+ deployment.status = finalStatus;
145
+ if (!report.success && report.failureReason) {
146
+ deployment.failureReason = report.failureReason;
147
+ }
148
+ deployment.completedAt = new Date();
149
+ deployments.save(deployment);
150
+ }
151
+
152
+ return reply.status(200).send({
153
+ accepted: true,
154
+ deploymentId: report.deploymentId,
155
+ envoyId: report.envoyId,
156
+ entriesIngested: ingested,
157
+ });
158
+ });
159
+ }
@@ -0,0 +1,237 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import type { ISettingsStore, ITelemetryStore, IDeploymentStore, DebriefReader } from "@synth-deploy/core";
3
+ import type { EnvoyRegistry } from "../agent/envoy-registry.js";
4
+ import { requirePermission } from "../middleware/permissions.js";
5
+ import { getMaxEnvoys, EditionError } from "@synth-deploy/core";
6
+
7
+ export function registerEnvoyRoutes(
8
+ app: FastifyInstance,
9
+ settings: ISettingsStore,
10
+ registry: EnvoyRegistry,
11
+ telemetry: ITelemetryStore,
12
+ deployments: IDeploymentStore,
13
+ debrief: DebriefReader,
14
+ ): void {
15
+ // List all registered Envoys (cached data — no live probe)
16
+ app.get("/api/envoys", { preHandler: [requirePermission("envoy.view")] }, async () => {
17
+ let entries = registry.listEntries();
18
+
19
+ // Also include the legacy settings-based default envoy if no registry entries
20
+ if (entries.length === 0) {
21
+ const envoyConfig = settings.get().envoy;
22
+ if (envoyConfig?.url) {
23
+ const legacy = registry.register({
24
+ name: "default",
25
+ url: envoyConfig.url,
26
+ });
27
+ const entry = registry.get(legacy.id);
28
+ if (entry) entries = [entry];
29
+ }
30
+ }
31
+
32
+ return {
33
+ envoys: entries.map((e) => ({
34
+ id: e.id,
35
+ name: e.name,
36
+ url: e.url,
37
+ health: e.health,
38
+ hostname: e.hostname,
39
+ os: e.os,
40
+ lastSeen: e.lastSeen,
41
+ summary: e.summary,
42
+ readiness: e.readiness,
43
+ assignedEnvironments: e.assignedEnvironments,
44
+ assignedPartitions: e.assignedPartitions,
45
+ })),
46
+ };
47
+ });
48
+
49
+ // Register a new Envoy
50
+ app.post("/api/envoys", { preHandler: [requirePermission("envoy.register")] }, async (request, reply) => {
51
+ const body = request.body as {
52
+ name: string;
53
+ url: string;
54
+ assignedEnvironments?: string[];
55
+ assignedPartitions?: string[];
56
+ };
57
+
58
+ if (!body.name || !body.url) {
59
+ return reply.status(400).send({ error: "name and url are required" });
60
+ }
61
+
62
+ // Enforce envoy count limit (Community: 10, Enterprise: license value)
63
+ const maxEnvoys = getMaxEnvoys();
64
+ if (maxEnvoys > 0 && registry.listEntries().length >= maxEnvoys) {
65
+ throw new EditionError("unlimited-envoys");
66
+ }
67
+
68
+ const registration = registry.register({
69
+ name: body.name,
70
+ url: body.url,
71
+ assignedEnvironments: body.assignedEnvironments,
72
+ assignedPartitions: body.assignedPartitions,
73
+ });
74
+
75
+ // Probe health immediately after registration
76
+ const entry = await registry.probe(registration.id);
77
+
78
+ telemetry.record({ actor: (request.user?.email) ?? "anonymous", action: "envoy.registered", target: { type: "envoy", id: registration.id }, details: { name: body.name, url: body.url } });
79
+
80
+ return reply.status(201).send({
81
+ envoy: {
82
+ id: registration.id,
83
+ name: registration.name,
84
+ url: registration.url,
85
+ token: registration.token,
86
+ assignedEnvironments: registration.assignedEnvironments,
87
+ assignedPartitions: registration.assignedPartitions,
88
+ registeredAt: registration.registeredAt,
89
+ health: entry?.health ?? "Unreachable",
90
+ },
91
+ });
92
+ });
93
+
94
+ // Get a specific Envoy's cached status (instant, no live probe)
95
+ app.get("/api/envoys/:id/health", { preHandler: [requirePermission("envoy.view")] }, async (request, reply) => {
96
+ const { id } = request.params as { id: string };
97
+ const entry = registry.get(id);
98
+
99
+ if (!entry) {
100
+ return reply.status(404).send({ error: "Envoy not found" });
101
+ }
102
+
103
+ return {
104
+ envoy: {
105
+ id: entry.id,
106
+ name: entry.name,
107
+ url: entry.url,
108
+ health: entry.health,
109
+ hostname: entry.hostname,
110
+ os: entry.os,
111
+ lastSeen: entry.lastSeen,
112
+ summary: entry.summary,
113
+ readiness: entry.readiness,
114
+ assignedEnvironments: entry.assignedEnvironments,
115
+ assignedPartitions: entry.assignedPartitions,
116
+ },
117
+ };
118
+ });
119
+
120
+ // Update an Envoy's configuration
121
+ app.put("/api/envoys/:id", { preHandler: [requirePermission("envoy.configure")] }, async (request, reply) => {
122
+ const { id } = request.params as { id: string };
123
+ const body = request.body as {
124
+ name?: string;
125
+ url?: string;
126
+ assignedEnvironments?: string[];
127
+ assignedPartitions?: string[];
128
+ };
129
+
130
+ const existing = registry.get(id);
131
+ const updated = registry.update(id, body);
132
+ if (!updated) {
133
+ return reply.status(404).send({ error: "Envoy not found" });
134
+ }
135
+
136
+ const actor = (request.user?.email) ?? "anonymous";
137
+ if (body.assignedEnvironments !== undefined && existing) {
138
+ const prev = new Set(existing.assignedEnvironments);
139
+ const next = new Set(body.assignedEnvironments);
140
+ for (const envId of next) {
141
+ if (!prev.has(envId)) telemetry.record({ actor, action: "envoy.connection.added", target: { type: "envoy", id }, details: { connectionType: "environment", targetId: envId } });
142
+ }
143
+ for (const envId of prev) {
144
+ if (!next.has(envId)) telemetry.record({ actor, action: "envoy.connection.removed", target: { type: "envoy", id }, details: { connectionType: "environment", targetId: envId } });
145
+ }
146
+ }
147
+ if (body.assignedPartitions !== undefined && existing) {
148
+ const prev = new Set(existing.assignedPartitions);
149
+ const next = new Set(body.assignedPartitions);
150
+ for (const partId of next) {
151
+ if (!prev.has(partId)) telemetry.record({ actor, action: "envoy.connection.added", target: { type: "envoy", id }, details: { connectionType: "partition", targetId: partId } });
152
+ }
153
+ for (const partId of prev) {
154
+ if (!next.has(partId)) telemetry.record({ actor, action: "envoy.connection.removed", target: { type: "envoy", id }, details: { connectionType: "partition", targetId: partId } });
155
+ }
156
+ }
157
+
158
+ return {
159
+ envoy: {
160
+ id: updated.id,
161
+ name: updated.name,
162
+ url: updated.url,
163
+ assignedEnvironments: updated.assignedEnvironments,
164
+ assignedPartitions: updated.assignedPartitions,
165
+ },
166
+ };
167
+ });
168
+
169
+ // Deregister an Envoy
170
+ app.delete("/api/envoys/:id", { preHandler: [requirePermission("envoy.configure")] }, async (request, reply) => {
171
+ const { id } = request.params as { id: string };
172
+ const removed = registry.deregister(id);
173
+
174
+ if (!removed) {
175
+ return reply.status(404).send({ error: "Envoy not found" });
176
+ }
177
+
178
+ return reply.status(204).send();
179
+ });
180
+
181
+ // Rotate an Envoy's token
182
+ app.post("/api/envoys/:id/rotate-token", { preHandler: [requirePermission("envoy.configure")] }, async (request, reply) => {
183
+ const { id } = request.params as { id: string };
184
+ const newToken = registry.rotateToken(id);
185
+
186
+ if (!newToken) {
187
+ return reply.status(404).send({ error: "Envoy not found" });
188
+ }
189
+
190
+ return { token: newToken };
191
+ });
192
+
193
+ // Get accumulated knowledge for an Envoy — system observations from environment scans
194
+ app.get("/api/envoys/:id/knowledge", { preHandler: [requirePermission("envoy.view")] }, async (request, reply) => {
195
+ const { id } = request.params as { id: string };
196
+ const entry = registry.get(id);
197
+ if (!entry) {
198
+ return reply.status(404).send({ error: "Envoy not found" });
199
+ }
200
+
201
+ const envoyDeployments = deployments.list().filter((d) => d.envoyId === id);
202
+ const observations: { id: string; timestamp: string; text: string }[] = [];
203
+
204
+ for (const d of envoyDeployments) {
205
+ const entries = debrief.getByDeployment(d.id);
206
+ for (const e of entries) {
207
+ if (e.decisionType === "environment-scan") {
208
+ observations.push({ id: e.id, timestamp: e.timestamp.toISOString(), text: e.reasoning || e.decision });
209
+ }
210
+ }
211
+ }
212
+
213
+ // Sort newest first, cap at 20
214
+ observations.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
215
+
216
+ return { knowledge: observations.slice(0, 20) };
217
+ });
218
+
219
+ // Validate an Envoy token (used by Envoy report endpoint)
220
+ app.post("/api/envoys/validate-token", async (request, reply) => {
221
+ const body = request.body as { token: string };
222
+ if (!body.token) {
223
+ return reply.status(400).send({ error: "token is required" });
224
+ }
225
+
226
+ const envoy = registry.validateToken(body.token);
227
+ if (!envoy) {
228
+ return reply.status(401).send({ error: "Invalid token" });
229
+ }
230
+
231
+ return {
232
+ valid: true,
233
+ envoyId: envoy.id,
234
+ envoyName: envoy.name,
235
+ };
236
+ });
237
+ }