@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,516 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import type { IArtifactStore } from "@synth-deploy/core";
3
+ import type { DeploymentPlan } from "@synth-deploy/core";
4
+ import type { DebriefWriter } from "@synth-deploy/core";
5
+ import { requirePermission, requireEdition } from "../middleware/permissions.js";
6
+ import type { DeploymentGraphStore } from "../graph/graph-store.js";
7
+ import type { GraphInferenceEngine } from "../graph/graph-inference.js";
8
+ import { GraphExecutor } from "../graph/graph-executor.js";
9
+ import type { EnvoyRegistry } from "../agent/envoy-registry.js";
10
+ import { EnvoyClient } from "../agent/envoy-client.js";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Deployment Graph API routes
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export function registerGraphRoutes(
17
+ app: FastifyInstance,
18
+ graphStore: DeploymentGraphStore,
19
+ inferenceEngine: GraphInferenceEngine,
20
+ envoyRegistry: EnvoyRegistry,
21
+ artifactStore: IArtifactStore,
22
+ debrief: DebriefWriter,
23
+ ): void {
24
+ // Create a deployment graph (triggers inference)
25
+ app.post(
26
+ "/api/deployment-graphs",
27
+ { preHandler: [requireEdition("deployment-graphs"), requirePermission("deployment.create")] },
28
+ async (request, reply) => {
29
+ const body = request.body as {
30
+ name?: string;
31
+ artifactIds: string[];
32
+ envoyAssignments: Record<string, string>;
33
+ partitionId?: string;
34
+ approvalMode?: "per-node" | "graph";
35
+ };
36
+
37
+ if (!body.artifactIds || body.artifactIds.length === 0) {
38
+ return reply
39
+ .status(400)
40
+ .send({ error: "artifactIds is required and must not be empty" });
41
+ }
42
+
43
+ if (!body.envoyAssignments || Object.keys(body.envoyAssignments).length === 0) {
44
+ return reply
45
+ .status(400)
46
+ .send({ error: "envoyAssignments is required" });
47
+ }
48
+
49
+ // Validate all artifacts exist
50
+ for (const artifactId of body.artifactIds) {
51
+ if (!artifactStore.get(artifactId)) {
52
+ return reply
53
+ .status(404)
54
+ .send({ error: `Artifact not found: ${artifactId}` });
55
+ }
56
+ }
57
+
58
+ // Validate all assigned envoys exist
59
+ for (const [artifactId, envoyId] of Object.entries(body.envoyAssignments)) {
60
+ if (!envoyRegistry.get(envoyId)) {
61
+ return reply
62
+ .status(404)
63
+ .send({
64
+ error: `Envoy not found: ${envoyId} (assigned to artifact ${artifactId})`,
65
+ });
66
+ }
67
+ }
68
+
69
+ // Infer the graph structure using LLM (or flat fallback)
70
+ const graph = await inferenceEngine.inferGraph({
71
+ artifactIds: body.artifactIds,
72
+ envoyAssignments: body.envoyAssignments,
73
+ partitionId: body.partitionId,
74
+ graphName: body.name,
75
+ });
76
+
77
+ if (body.approvalMode) {
78
+ graph.approvalMode = body.approvalMode;
79
+ }
80
+
81
+ graphStore.create(graph);
82
+
83
+ // Record debrief entry for graph creation with inference reasoning
84
+ debrief.record({
85
+ partitionId: graph.partitionId ?? null,
86
+ deploymentId: null,
87
+ agent: "server",
88
+ decisionType: "plan-generation",
89
+ decision: `Created deployment graph "${graph.name}" with ${graph.nodes.length} nodes and ${graph.edges.length} edges`,
90
+ reasoning: `Inferred dependency graph for artifacts: ${body.artifactIds.join(", ")}. Edges: ${JSON.stringify(graph.edges.map((e) => `${e.from} -[${e.type}]-> ${e.to}`))}`,
91
+ context: {
92
+ graphId: graph.id,
93
+ nodeCount: graph.nodes.length,
94
+ edgeCount: graph.edges.length,
95
+ approvalMode: graph.approvalMode,
96
+ edges: graph.edges,
97
+ },
98
+ });
99
+
100
+ return reply.status(201).send({ graph });
101
+ },
102
+ );
103
+
104
+ // List all deployment graphs
105
+ app.get(
106
+ "/api/deployment-graphs",
107
+ { preHandler: [requireEdition("deployment-graphs"), requirePermission("deployment.view")] },
108
+ async () => {
109
+ return { graphs: graphStore.list() };
110
+ },
111
+ );
112
+
113
+ // Get a specific deployment graph with node statuses
114
+ app.get(
115
+ "/api/deployment-graphs/:id",
116
+ { preHandler: [requireEdition("deployment-graphs"), requirePermission("deployment.view")] },
117
+ async (request, reply) => {
118
+ const { id } = request.params as { id: string };
119
+ const graph = graphStore.getById(id);
120
+
121
+ if (!graph) {
122
+ return reply.status(404).send({ error: "Deployment graph not found" });
123
+ }
124
+
125
+ // Enrich with artifact names for display
126
+ const enrichedNodes = graph.nodes.map((node) => {
127
+ const artifact = artifactStore.get(node.artifactId);
128
+ const envoy = envoyRegistry.get(node.envoyId);
129
+ return {
130
+ ...node,
131
+ artifactName: artifact?.name ?? node.artifactId,
132
+ envoyName: envoy?.name ?? node.envoyId,
133
+ };
134
+ });
135
+
136
+ return { graph: { ...graph, nodes: enrichedNodes } };
137
+ },
138
+ );
139
+
140
+ // Update a deployment graph (user corrections to inferred structure)
141
+ app.put(
142
+ "/api/deployment-graphs/:id",
143
+ { preHandler: [requireEdition("deployment-graphs"), requirePermission("deployment.create")] },
144
+ async (request, reply) => {
145
+ const { id } = request.params as { id: string };
146
+ const graph = graphStore.getById(id);
147
+ if (!graph) {
148
+ return reply.status(404).send({ error: "Deployment graph not found" });
149
+ }
150
+
151
+ const body = request.body as {
152
+ name?: string;
153
+ nodes?: typeof graph.nodes;
154
+ edges?: typeof graph.edges;
155
+ approvalMode?: "per-node" | "graph";
156
+ };
157
+
158
+ if (graph.status !== "draft" && graph.status !== "awaiting_approval") {
159
+ return reply
160
+ .status(409)
161
+ .send({ error: `Cannot modify graph in status: ${graph.status}` });
162
+ }
163
+
164
+ const updated = graphStore.update(id, body);
165
+
166
+ // Record debrief entry for user corrections
167
+ debrief.record({
168
+ partitionId: graph.partitionId ?? null,
169
+ deploymentId: null,
170
+ agent: "server",
171
+ decisionType: "plan-modification",
172
+ decision: `User corrected deployment graph "${graph.name}"`,
173
+ reasoning: `Updated fields: ${Object.keys(body).filter((k) => body[k as keyof typeof body] !== undefined).join(", ")}`,
174
+ context: {
175
+ graphId: id,
176
+ updatedFields: Object.keys(body).filter(
177
+ (k) => body[k as keyof typeof body] !== undefined,
178
+ ),
179
+ },
180
+ });
181
+
182
+ return { graph: updated };
183
+ },
184
+ );
185
+
186
+ // Delete a deployment graph
187
+ app.delete(
188
+ "/api/deployment-graphs/:id",
189
+ { preHandler: [requireEdition("deployment-graphs"), requirePermission("deployment.create")] },
190
+ async (request, reply) => {
191
+ const { id } = request.params as { id: string };
192
+
193
+ const graph = graphStore.getById(id);
194
+ if (!graph) {
195
+ return reply.status(404).send({ error: "Deployment graph not found" });
196
+ }
197
+
198
+ if (graph.status === "executing") {
199
+ return reply
200
+ .status(409)
201
+ .send({ error: "Cannot delete a graph that is currently executing" });
202
+ }
203
+
204
+ graphStore.delete(id);
205
+ return reply.status(204).send();
206
+ },
207
+ );
208
+
209
+ // Execute a deployment graph
210
+ app.post(
211
+ "/api/deployment-graphs/:id/execute",
212
+ { preHandler: [requireEdition("deployment-graphs"), requirePermission("deployment.approve")] },
213
+ async (request, reply) => {
214
+ const { id } = request.params as { id: string };
215
+ const body = request.body as {
216
+ plans: Record<string, DeploymentPlan>; // nodeId -> plan
217
+ partitionVariables?: Record<string, string>;
218
+ };
219
+
220
+ const graph = graphStore.getById(id);
221
+ if (!graph) {
222
+ return reply.status(404).send({ error: "Deployment graph not found" });
223
+ }
224
+
225
+ if (
226
+ graph.status !== "draft" &&
227
+ graph.status !== "awaiting_approval"
228
+ ) {
229
+ return reply
230
+ .status(409)
231
+ .send({ error: `Cannot execute graph in status: ${graph.status}` });
232
+ }
233
+
234
+ if (!body.plans || Object.keys(body.plans).length === 0) {
235
+ return reply
236
+ .status(400)
237
+ .send({ error: "plans map (nodeId -> DeploymentPlan) is required" });
238
+ }
239
+
240
+ // Validate all nodes have plans
241
+ const missingPlans = graph.nodes
242
+ .filter((n) => !body.plans[n.id])
243
+ .map((n) => n.id);
244
+ if (missingPlans.length > 0) {
245
+ return reply.status(400).send({
246
+ error: `Missing plans for nodes: ${missingPlans.join(", ")}`,
247
+ });
248
+ }
249
+
250
+ graphStore.updateStatus(id, "executing");
251
+
252
+ const executor = new GraphExecutor(
253
+ envoyRegistry,
254
+ (url, timeoutMs) => new EnvoyClient(url, timeoutMs),
255
+ );
256
+
257
+ const plansMap = new Map(Object.entries(body.plans));
258
+ const events: Array<Record<string, unknown>> = [];
259
+ const dataFlowValues: Record<string, Record<string, string>> = {};
260
+
261
+ // Execute and collect results
262
+ // (In a production system this would be async with SSE/WebSocket progress,
263
+ // but for now we run synchronously and return the final state.)
264
+ try {
265
+ for await (const event of executor.execute(
266
+ graph,
267
+ plansMap,
268
+ body.partitionVariables,
269
+ )) {
270
+ events.push(event as unknown as Record<string, unknown>);
271
+
272
+ // Update node status in store
273
+ if (event.nodeId) {
274
+ if (event.type === "node-started") {
275
+ graphStore.updateNode(id, event.nodeId, { status: "executing" });
276
+ } else if (event.type === "node-completed") {
277
+ graphStore.updateNode(id, event.nodeId, { status: "completed" });
278
+ if (event.outputCapture) {
279
+ dataFlowValues[event.nodeId] = event.outputCapture;
280
+ }
281
+ } else if (event.type === "node-failed") {
282
+ graphStore.updateNode(id, event.nodeId, { status: "failed" });
283
+ } else if (event.type === "node-skipped") {
284
+ // Skipped nodes remain in "pending" status — they were never started
285
+ graphStore.updateNode(id, event.nodeId, { status: "pending" });
286
+ }
287
+ }
288
+
289
+ if (event.type === "graph-completed") {
290
+ graphStore.updateStatus(id, "completed");
291
+ } else if (event.type === "graph-failed") {
292
+ graphStore.updateStatus(id, "failed");
293
+ }
294
+ }
295
+ } catch (err) {
296
+ graphStore.updateStatus(id, "failed");
297
+ const message = err instanceof Error ? err.message : String(err);
298
+ return reply.status(500).send({ error: message, events });
299
+ }
300
+
301
+ const finalGraph = graphStore.getById(id);
302
+
303
+ // Record debrief summary for execution completion
304
+ const finalCompletedCount =
305
+ finalGraph?.nodes.filter((n) => n.status === "completed").length ?? 0;
306
+ const finalFailedCount =
307
+ finalGraph?.nodes.filter((n) => n.status === "failed").length ?? 0;
308
+
309
+ debrief.record({
310
+ partitionId: graph.partitionId ?? null,
311
+ deploymentId: null,
312
+ agent: "server",
313
+ decisionType:
314
+ finalFailedCount > 0 ? "deployment-failure" : "deployment-completion",
315
+ decision: `Graph "${graph.name}" execution ${finalFailedCount > 0 ? "failed" : "completed"}: ${finalCompletedCount}/${graph.nodes.length} nodes succeeded`,
316
+ reasoning: `Executed deployment graph with ${graph.nodes.length} nodes. ${finalCompletedCount} completed, ${finalFailedCount} failed. Data flow values captured for ${Object.keys(dataFlowValues).length} nodes.`,
317
+ context: {
318
+ graphId: id,
319
+ completedCount: finalCompletedCount,
320
+ failedCount: finalFailedCount,
321
+ totalNodes: graph.nodes.length,
322
+ dataFlowValues,
323
+ partitionVariables: body.partitionVariables,
324
+ },
325
+ });
326
+
327
+ return { graph: finalGraph, events };
328
+ },
329
+ );
330
+
331
+ // Rollback a deployment graph
332
+ app.post(
333
+ "/api/deployment-graphs/:id/rollback",
334
+ { preHandler: [requireEdition("deployment-graphs"), requirePermission("deployment.rollback")] },
335
+ async (request, reply) => {
336
+ const { id } = request.params as { id: string };
337
+ const body = request.body as {
338
+ rollbackPlans: Record<string, DeploymentPlan>;
339
+ };
340
+
341
+ const graph = graphStore.getById(id);
342
+ if (!graph) {
343
+ return reply.status(404).send({ error: "Deployment graph not found" });
344
+ }
345
+
346
+ if (graph.status !== "completed" && graph.status !== "failed") {
347
+ return reply.status(409).send({
348
+ error: `Cannot rollback graph in status: ${graph.status}`,
349
+ });
350
+ }
351
+
352
+ if (!body.rollbackPlans || Object.keys(body.rollbackPlans).length === 0) {
353
+ return reply
354
+ .status(400)
355
+ .send({ error: "rollbackPlans map is required" });
356
+ }
357
+
358
+ graphStore.updateStatus(id, "executing");
359
+
360
+ const executor = new GraphExecutor(
361
+ envoyRegistry,
362
+ (url, timeoutMs) => new EnvoyClient(url, timeoutMs),
363
+ );
364
+
365
+ const rollbackMap = new Map(Object.entries(body.rollbackPlans));
366
+ const events: Array<Record<string, unknown>> = [];
367
+
368
+ try {
369
+ for await (const event of executor.rollback(graph, rollbackMap)) {
370
+ events.push(event as unknown as Record<string, unknown>);
371
+ }
372
+ } catch {
373
+ // Rollback errors are non-fatal — we still update status
374
+ }
375
+
376
+ graphStore.updateStatus(id, "rolled_back");
377
+ const finalGraph = graphStore.getById(id);
378
+ return { graph: finalGraph, events };
379
+ },
380
+ );
381
+
382
+ // Per-node approval (for per-node approval mode)
383
+ app.post(
384
+ "/api/deployment-graphs/:id/nodes/:nodeId/approve",
385
+ { preHandler: [requireEdition("deployment-graphs"), requirePermission("deployment.approve")] },
386
+ async (request, reply) => {
387
+ const { id, nodeId } = request.params as { id: string; nodeId: string };
388
+
389
+ const graph = graphStore.getById(id);
390
+ if (!graph) {
391
+ return reply.status(404).send({ error: "Deployment graph not found" });
392
+ }
393
+
394
+ if (graph.approvalMode !== "per-node") {
395
+ return reply.status(409).send({
396
+ error: "Graph is not in per-node approval mode",
397
+ });
398
+ }
399
+
400
+ const existingNode = graph.nodes.find((n) => n.id === nodeId);
401
+ if (!existingNode) {
402
+ return reply.status(404).send({ error: "Node not found" });
403
+ }
404
+
405
+ if (existingNode.status !== "awaiting_approval") {
406
+ return reply.status(409).send({
407
+ error: `Node is not awaiting approval (current status: ${existingNode.status})`,
408
+ });
409
+ }
410
+
411
+ // Mark as approved (ready to execute)
412
+ const node = graphStore.updateNode(id, nodeId, { status: "pending" });
413
+
414
+ return { node };
415
+ },
416
+ );
417
+
418
+ // Approve remaining nodes — switch from per-node to graph approval mid-execution
419
+ app.post(
420
+ "/api/deployment-graphs/:id/approve-remaining",
421
+ { preHandler: [requireEdition("deployment-graphs"), requirePermission("deployment.approve")] },
422
+ async (request, reply) => {
423
+ const { id } = request.params as { id: string };
424
+
425
+ const graph = graphStore.getById(id);
426
+ if (!graph) {
427
+ return reply.status(404).send({ error: "Deployment graph not found" });
428
+ }
429
+
430
+ if (graph.status !== "executing" && graph.status !== "awaiting_approval") {
431
+ return reply.status(409).send({
432
+ error: `Cannot approve remaining in status: ${graph.status}`,
433
+ });
434
+ }
435
+
436
+ graphStore.update(id, { approvalMode: "graph" });
437
+
438
+ return { graph: graphStore.getById(id) };
439
+ },
440
+ );
441
+
442
+ // Retry a failed node
443
+ app.post(
444
+ "/api/deployment-graphs/:id/nodes/:nodeId/retry",
445
+ { preHandler: [requireEdition("deployment-graphs"), requirePermission("deployment.approve")] },
446
+ async (request, reply) => {
447
+ const { id, nodeId } = request.params as { id: string; nodeId: string };
448
+
449
+ const graph = graphStore.getById(id);
450
+ if (!graph) {
451
+ return reply.status(404).send({ error: "Deployment graph not found" });
452
+ }
453
+
454
+ if (graph.status !== "failed") {
455
+ return reply.status(409).send({
456
+ error: `Cannot retry node when graph status is: ${graph.status}`,
457
+ });
458
+ }
459
+
460
+ const existingNode = graph.nodes.find((n) => n.id === nodeId);
461
+ if (!existingNode) {
462
+ return reply.status(404).send({ error: "Node not found" });
463
+ }
464
+
465
+ if (existingNode.status !== "failed") {
466
+ return reply.status(409).send({
467
+ error: `Node is not in failed status (current status: ${existingNode.status})`,
468
+ });
469
+ }
470
+
471
+ // Re-queue for execution
472
+ const node = graphStore.updateNode(id, nodeId, { status: "pending" });
473
+
474
+ return { node };
475
+ },
476
+ );
477
+
478
+ // Skip a failed node — allows downstream to proceed
479
+ app.post(
480
+ "/api/deployment-graphs/:id/nodes/:nodeId/skip",
481
+ { preHandler: [requireEdition("deployment-graphs"), requirePermission("deployment.approve")] },
482
+ async (request, reply) => {
483
+ const { id, nodeId } = request.params as { id: string; nodeId: string };
484
+
485
+ const graph = graphStore.getById(id);
486
+ if (!graph) {
487
+ return reply.status(404).send({ error: "Deployment graph not found" });
488
+ }
489
+
490
+ if (graph.status !== "failed") {
491
+ return reply.status(409).send({
492
+ error: `Cannot skip node when graph status is: ${graph.status}`,
493
+ });
494
+ }
495
+
496
+ const existingNode = graph.nodes.find((n) => n.id === nodeId);
497
+ if (!existingNode) {
498
+ return reply.status(404).send({ error: "Node not found" });
499
+ }
500
+
501
+ if (existingNode.status !== "failed") {
502
+ return reply.status(409).send({
503
+ error: `Node is not in failed status (current status: ${existingNode.status})`,
504
+ });
505
+ }
506
+
507
+ // Mark as completed (skipped) so downstream nodes can proceed
508
+ const node = graphStore.updateNode(id, nodeId, { status: "completed" });
509
+
510
+ // Set graph back to awaiting_approval so it can be re-executed
511
+ graphStore.updateStatus(id, "awaiting_approval");
512
+
513
+ return { node, skipped: true, graph: graphStore.getById(id) };
514
+ },
515
+ );
516
+ }