@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,309 @@
1
+ import type {
2
+ FleetDeployment,
3
+ FleetProgress,
4
+ FleetValidationResult,
5
+ EnvoyValidationResult,
6
+ DeploymentPlan,
7
+ RolloutConfig,
8
+ } from "@synth-deploy/core";
9
+ import type { EnvoyRegistry } from "../agent/envoy-registry.js";
10
+ import { EnvoyClient } from "../agent/envoy-client.js";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Fleet progress event types
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export interface FleetProgressEvent {
17
+ type:
18
+ | "validation-started"
19
+ | "validation-complete"
20
+ | "batch-started"
21
+ | "envoy-started"
22
+ | "envoy-completed"
23
+ | "envoy-failed"
24
+ | "batch-completed"
25
+ | "fleet-completed"
26
+ | "fleet-failed"
27
+ | "fleet-paused";
28
+ envoyId?: string;
29
+ envoyName?: string;
30
+ batchIndex?: number;
31
+ progress: FleetProgress;
32
+ error?: string;
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Helpers
37
+ // ---------------------------------------------------------------------------
38
+
39
+ function delay(ms: number): Promise<void> {
40
+ return new Promise((resolve) => setTimeout(resolve, ms));
41
+ }
42
+
43
+ function makeProgress(
44
+ totalEnvoys: number,
45
+ validated: number,
46
+ executing: number,
47
+ succeeded: number,
48
+ failed: number,
49
+ currentBatch?: number,
50
+ totalBatches?: number,
51
+ ): FleetProgress {
52
+ return {
53
+ totalEnvoys,
54
+ validated,
55
+ executing,
56
+ succeeded,
57
+ failed,
58
+ pending: totalEnvoys - succeeded - failed - executing,
59
+ currentBatch,
60
+ totalBatches,
61
+ };
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // FleetExecutor — orchestrates validation and progressive rollout
66
+ // ---------------------------------------------------------------------------
67
+
68
+ export class FleetExecutor {
69
+ constructor(
70
+ private envoyRegistry: EnvoyRegistry,
71
+ private createEnvoyClient: (url: string, token: string) => EnvoyClient,
72
+ ) {}
73
+
74
+ /**
75
+ * Validate a deployment plan against all target envoys in the fleet.
76
+ * Probes each envoy and asks it to validate the plan steps.
77
+ */
78
+ async validateFleet(
79
+ fleetDeployment: FleetDeployment,
80
+ plan: DeploymentPlan,
81
+ ): Promise<FleetValidationResult> {
82
+ const targetEnvoyIds =
83
+ fleetDeployment.envoyFilter ?? this.getEnvironmentEnvoyIds(fleetDeployment.environmentId);
84
+
85
+ const results: EnvoyValidationResult[] = [];
86
+
87
+ for (const envoyId of targetEnvoyIds) {
88
+ const entry = this.envoyRegistry.get(envoyId);
89
+ if (!entry) {
90
+ results.push({
91
+ envoyId,
92
+ envoyName: "unknown",
93
+ validated: false,
94
+ issues: [`Envoy ${envoyId} not found in registry`],
95
+ });
96
+ continue;
97
+ }
98
+
99
+ const client = this.createEnvoyClient(entry.url, entry.token);
100
+ try {
101
+ const validation = await client.validatePlan(plan.steps);
102
+ results.push({
103
+ envoyId,
104
+ envoyName: entry.name,
105
+ validated: validation.valid,
106
+ issues: validation.violations?.map((v) => v.reason),
107
+ });
108
+ } catch (err) {
109
+ results.push({
110
+ envoyId,
111
+ envoyName: entry.name,
112
+ validated: false,
113
+ issues: [
114
+ `Validation failed: ${err instanceof Error ? err.message : String(err)}`,
115
+ ],
116
+ });
117
+ }
118
+ }
119
+
120
+ return {
121
+ total: results.length,
122
+ validated: results.filter((r) => r.validated).length,
123
+ failed: results.filter((r) => !r.validated).length,
124
+ results,
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Execute a progressive rollout across the fleet.
130
+ * Yields progress events as an async generator so callers can stream them.
131
+ */
132
+ async *executeRollout(
133
+ fleetDeployment: FleetDeployment,
134
+ plan: DeploymentPlan,
135
+ rollbackPlan?: DeploymentPlan,
136
+ ): AsyncGenerator<FleetProgressEvent> {
137
+ const validatedEnvoys =
138
+ fleetDeployment.validationResult?.results.filter((r) => r.validated) ?? [];
139
+ const config = fleetDeployment.rolloutConfig;
140
+ const totalEnvoys = validatedEnvoys.length;
141
+
142
+ if (totalEnvoys === 0) {
143
+ yield {
144
+ type: "fleet-failed",
145
+ progress: makeProgress(0, 0, 0, 0, 0),
146
+ error: "No validated envoys to execute against",
147
+ };
148
+ return;
149
+ }
150
+
151
+ // Build batches based on rollout strategy
152
+ const batches = this.buildBatches(validatedEnvoys, config);
153
+ const totalBatches = batches.length;
154
+
155
+ let succeeded = 0;
156
+ let failureCount = 0;
157
+
158
+ for (let i = 0; i < batches.length; i++) {
159
+ yield {
160
+ type: "batch-started",
161
+ batchIndex: i,
162
+ progress: makeProgress(totalEnvoys, totalEnvoys, batches[i].length, succeeded, failureCount, i, totalBatches),
163
+ };
164
+
165
+ for (const envoy of batches[i]) {
166
+ yield {
167
+ type: "envoy-started",
168
+ envoyId: envoy.envoyId,
169
+ envoyName: envoy.envoyName,
170
+ batchIndex: i,
171
+ progress: makeProgress(totalEnvoys, totalEnvoys, 1, succeeded, failureCount, i, totalBatches),
172
+ };
173
+
174
+ const entry = this.envoyRegistry.get(envoy.envoyId);
175
+ if (!entry) {
176
+ failureCount++;
177
+ yield {
178
+ type: "envoy-failed",
179
+ envoyId: envoy.envoyId,
180
+ envoyName: envoy.envoyName,
181
+ batchIndex: i,
182
+ progress: makeProgress(totalEnvoys, totalEnvoys, 0, succeeded, failureCount, i, totalBatches),
183
+ error: `Envoy ${envoy.envoyId} not found in registry`,
184
+ };
185
+
186
+ if (failureCount >= config.haltOnFailureCount) {
187
+ yield {
188
+ type: "fleet-failed",
189
+ progress: makeProgress(totalEnvoys, totalEnvoys, 0, succeeded, failureCount, i, totalBatches),
190
+ error: `Halted: ${failureCount} failure(s) reached threshold of ${config.haltOnFailureCount}`,
191
+ };
192
+ return;
193
+ }
194
+ continue;
195
+ }
196
+
197
+ const client = this.createEnvoyClient(entry.url, entry.token);
198
+ try {
199
+ await client.executeApprovedPlan({
200
+ deploymentId: fleetDeployment.id,
201
+ plan,
202
+ rollbackPlan: rollbackPlan ?? plan,
203
+ artifactType: "fleet",
204
+ artifactName: fleetDeployment.artifactId,
205
+ environmentId: fleetDeployment.environmentId,
206
+ });
207
+ succeeded++;
208
+ yield {
209
+ type: "envoy-completed",
210
+ envoyId: envoy.envoyId,
211
+ envoyName: envoy.envoyName,
212
+ batchIndex: i,
213
+ progress: makeProgress(totalEnvoys, totalEnvoys, 0, succeeded, failureCount, i, totalBatches),
214
+ };
215
+ } catch (err) {
216
+ failureCount++;
217
+ yield {
218
+ type: "envoy-failed",
219
+ envoyId: envoy.envoyId,
220
+ envoyName: envoy.envoyName,
221
+ batchIndex: i,
222
+ progress: makeProgress(totalEnvoys, totalEnvoys, 0, succeeded, failureCount, i, totalBatches),
223
+ error: err instanceof Error ? err.message : String(err),
224
+ };
225
+
226
+ if (failureCount >= config.haltOnFailureCount) {
227
+ yield {
228
+ type: "fleet-failed",
229
+ progress: makeProgress(totalEnvoys, totalEnvoys, 0, succeeded, failureCount, i, totalBatches),
230
+ error: `Halted: ${failureCount} failure(s) reached threshold of ${config.haltOnFailureCount}`,
231
+ };
232
+ return;
233
+ }
234
+ }
235
+ }
236
+
237
+ yield {
238
+ type: "batch-completed",
239
+ batchIndex: i,
240
+ progress: makeProgress(totalEnvoys, totalEnvoys, 0, succeeded, failureCount, i, totalBatches),
241
+ };
242
+
243
+ // Health check wait between batches
244
+ if (config.healthCheckWaitMs > 0 && i < batches.length - 1) {
245
+ await delay(config.healthCheckWaitMs);
246
+ }
247
+
248
+ // Pause between batches if configured — yield paused event and return.
249
+ // Caller resumes by calling executeRollout again with remaining batches.
250
+ if (config.pauseBetweenBatches && i < batches.length - 1) {
251
+ yield {
252
+ type: "fleet-paused",
253
+ batchIndex: i,
254
+ progress: makeProgress(totalEnvoys, totalEnvoys, 0, succeeded, failureCount, i, totalBatches),
255
+ };
256
+ return;
257
+ }
258
+ }
259
+
260
+ yield {
261
+ type: "fleet-completed",
262
+ progress: makeProgress(totalEnvoys, totalEnvoys, 0, succeeded, failureCount, totalBatches - 1, totalBatches),
263
+ };
264
+ }
265
+
266
+ /**
267
+ * Build execution batches based on the rollout strategy.
268
+ */
269
+ private buildBatches(
270
+ envoys: EnvoyValidationResult[],
271
+ config: RolloutConfig,
272
+ ): EnvoyValidationResult[][] {
273
+ if (envoys.length === 0) return [];
274
+
275
+ if (config.strategy === "all-at-once") {
276
+ return [envoys];
277
+ }
278
+
279
+ if (config.strategy === "canary") {
280
+ // First batch: single canary, second batch: everything else
281
+ return [[envoys[0]], envoys.slice(1)];
282
+ }
283
+
284
+ // batched strategy
285
+ const size =
286
+ config.batchSize ??
287
+ Math.max(1, Math.ceil(envoys.length * (config.batchPercent ?? 10) / 100));
288
+
289
+ const batches: EnvoyValidationResult[][] = [];
290
+ for (let i = 0; i < envoys.length; i += size) {
291
+ batches.push(envoys.slice(i, i + size));
292
+ }
293
+ return batches;
294
+ }
295
+
296
+ /**
297
+ * Get all envoy IDs assigned to a given environment.
298
+ */
299
+ private getEnvironmentEnvoyIds(environmentId: string): string[] {
300
+ return this.envoyRegistry
301
+ .list()
302
+ .filter(
303
+ (e) =>
304
+ e.assignedEnvironments.length === 0 ||
305
+ e.assignedEnvironments.includes(environmentId),
306
+ )
307
+ .map((e) => e.id);
308
+ }
309
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Re-exports the persistent fleet deployment store from @synth-deploy/core.
3
+ * Fleet deployments are now SQLite-backed to survive server restarts.
4
+ *
5
+ * Note: In-flight fleet operations that are mid-execution when the server
6
+ * crashes cannot be resumed — their status is persisted, but the active
7
+ * orchestration state (batch progress, in-flight envoy connections) is
8
+ * ephemeral. On restart, in-flight operations will appear as stale entries
9
+ * that users can inspect and manually re-trigger. Terminal states (completed,
10
+ * failed, rolled_back) are fully durable.
11
+ */
12
+
13
+ export { PersistentFleetDeploymentStore as FleetDeploymentStore } from "@synth-deploy/core";
@@ -0,0 +1,4 @@
1
+ export { selectRepresentatives } from "./representative-selector.js";
2
+ export { FleetExecutor } from "./fleet-executor.js";
3
+ export type { FleetProgressEvent } from "./fleet-executor.js";
4
+ export { FleetDeploymentStore } from "./fleet-store.js";
@@ -0,0 +1,83 @@
1
+ import type { EnvoyRegistryEntry } from "../agent/envoy-registry.js";
2
+
3
+ /**
4
+ * Maximum number of representative envoys to select for plan validation.
5
+ * Keeps validation fast while covering platform diversity.
6
+ */
7
+ const MAX_REPRESENTATIVES = 3;
8
+
9
+ /**
10
+ * Select a representative subset of envoys for plan validation.
11
+ *
12
+ * Strategy:
13
+ * 1. Filter to healthy envoys only (health === "OK")
14
+ * 2. Group by platform (e.g. linux, darwin, windows)
15
+ * 3. Pick one per platform group, preferring most recently seen
16
+ * 4. Cap at MAX_REPRESENTATIVES
17
+ * 5. If all envoys share the same platform, pick 1
18
+ *
19
+ * Returns an array of envoy IDs.
20
+ */
21
+ export function selectRepresentatives(
22
+ envoys: EnvoyRegistryEntry[],
23
+ _artifactId: string,
24
+ ): string[] {
25
+ // 1. Filter to healthy envoys
26
+ const healthy = envoys.filter((e) => e.health === "OK");
27
+ if (healthy.length === 0) return [];
28
+
29
+ // 2. Group by platform
30
+ const byPlatform = new Map<string, EnvoyRegistryEntry[]>();
31
+ for (const envoy of healthy) {
32
+ // Normalize: null/undefined platform treated as "unknown"
33
+ const platform = (envoy as { platform?: string | null }).platform ?? "unknown";
34
+ const group = byPlatform.get(platform) ?? [];
35
+ group.push(envoy);
36
+ byPlatform.set(platform, group);
37
+ }
38
+
39
+ // 3. If all same platform, pick the most recently seen one
40
+ if (byPlatform.size <= 1) {
41
+ const best = pickMostRecentlySeen(healthy);
42
+ return best ? [best.id] : [];
43
+ }
44
+
45
+ // 4. Pick one per platform group (prefer most recently seen)
46
+ const representatives: EnvoyRegistryEntry[] = [];
47
+ for (const group of byPlatform.values()) {
48
+ const best = pickMostRecentlySeen(group);
49
+ if (best) representatives.push(best);
50
+ }
51
+
52
+ // 5. Cap at MAX_REPRESENTATIVES (sort by lastSeen descending, take top N)
53
+ representatives.sort((a, b) => {
54
+ const aTime = a.lastSeen ? new Date(a.lastSeen).getTime() : 0;
55
+ const bTime = b.lastSeen ? new Date(b.lastSeen).getTime() : 0;
56
+ return bTime - aTime;
57
+ });
58
+
59
+ return representatives.slice(0, MAX_REPRESENTATIVES).map((e) => e.id);
60
+ }
61
+
62
+ /**
63
+ * Pick the envoy with the most recent `lastSeen` timestamp from a group.
64
+ */
65
+ function pickMostRecentlySeen(
66
+ group: EnvoyRegistryEntry[],
67
+ ): EnvoyRegistryEntry | undefined {
68
+ if (group.length === 0) return undefined;
69
+ if (group.length === 1) return group[0];
70
+
71
+ let best = group[0];
72
+ let bestTime = best.lastSeen ? new Date(best.lastSeen).getTime() : 0;
73
+
74
+ for (let i = 1; i < group.length; i++) {
75
+ const t = group[i].lastSeen ? new Date(group[i].lastSeen!).getTime() : 0;
76
+ if (t > bestTime) {
77
+ best = group[i];
78
+ bestTime = t;
79
+ }
80
+ }
81
+
82
+ return best;
83
+ }