@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,474 @@
1
+ import type {
2
+ ServiceHealthChecker,
3
+ HealthCheckResult,
4
+ } from "./health-checker.js";
5
+ import { serverLog, serverWarn, serverError } from "../logger.js";
6
+ import type { DecisionType, DeploymentPlan, PlannedStep, SecurityBoundary } from "@synth-deploy/core";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Types — Envoy API responses
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export interface EnvoyDeployResult {
13
+ deploymentId: string;
14
+ success: boolean;
15
+ workspacePath: string;
16
+ artifacts: string[];
17
+ executionDurationMs: number;
18
+ totalDurationMs: number;
19
+ verificationPassed: boolean;
20
+ verificationChecks: Array<{ name: string; passed: boolean; detail: string }>;
21
+ failureReason: string | null;
22
+ debriefEntryIds: string[];
23
+ /** Full debrief entries from the Envoy — Command can ingest these */
24
+ debriefEntries: Array<{
25
+ id: string;
26
+ timestamp: string;
27
+ partitionId: string | null;
28
+ deploymentId: string | null;
29
+ agent: "server" | "envoy";
30
+ decisionType: DecisionType;
31
+ decision: string;
32
+ reasoning: string;
33
+ context: Record<string, unknown>;
34
+ }>;
35
+ }
36
+
37
+ export interface EnvoyHealthResponse {
38
+ status: "healthy" | "degraded";
39
+ service: string;
40
+ hostname: string;
41
+ os: string;
42
+ timestamp: string;
43
+ readiness: { ready: boolean; reason: string };
44
+ summary: {
45
+ totalDeployments: number;
46
+ succeeded: number;
47
+ failed: number;
48
+ executing: number;
49
+ environments: number;
50
+ };
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Retry with exponential backoff
55
+ // ---------------------------------------------------------------------------
56
+
57
+ interface RetryOptions {
58
+ maxRetries: number;
59
+ baseDelayMs: number;
60
+ retryableStatuses: Set<number>;
61
+ }
62
+
63
+ const DEFAULT_RETRY: RetryOptions = {
64
+ maxRetries: Number(process.env.SYNTH_ENVOY_MAX_RETRIES ?? 3),
65
+ baseDelayMs: 1000,
66
+ retryableStatuses: new Set([502, 503, 504]),
67
+ };
68
+
69
+ function isTransientError(err: unknown): boolean {
70
+ if (!(err instanceof Error)) return false;
71
+ const msg = err.message;
72
+ return (
73
+ msg.includes("ECONNREFUSED") ||
74
+ msg.includes("ECONNRESET") ||
75
+ msg.includes("fetch failed") ||
76
+ msg.includes("abort") ||
77
+ msg.includes("ETIMEDOUT") ||
78
+ msg.includes("network")
79
+ );
80
+ }
81
+
82
+ async function sleep(ms: number): Promise<void> {
83
+ return new Promise((resolve) => setTimeout(resolve, ms));
84
+ }
85
+
86
+ async function fetchWithRetry(
87
+ url: string,
88
+ init: RequestInit,
89
+ timeoutMs: number,
90
+ opts: RetryOptions = DEFAULT_RETRY,
91
+ ): Promise<Response> {
92
+ let lastError: Error | undefined;
93
+
94
+ for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
95
+ const controller = new AbortController();
96
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
97
+
98
+ try {
99
+ const response = await fetch(url, {
100
+ ...init,
101
+ signal: controller.signal,
102
+ });
103
+
104
+ // If retryable status code and we have retries left, retry
105
+ if (opts.retryableStatuses.has(response.status) && attempt < opts.maxRetries) {
106
+ const delay = opts.baseDelayMs * Math.pow(2, attempt);
107
+ console.log(`[envoy-client] Retryable HTTP ${response.status} on attempt ${attempt + 1}/${opts.maxRetries + 1}, retrying in ${delay}ms`);
108
+ serverWarn(`HTTP-RETRY`, { url, status: response.status, attempt: attempt + 1, maxAttempts: opts.maxRetries + 1, delayMs: delay });
109
+ await sleep(delay);
110
+ continue;
111
+ }
112
+
113
+ return response;
114
+ } catch (err) {
115
+ lastError = err instanceof Error ? err : new Error(String(err));
116
+
117
+ if (isTransientError(err) && attempt < opts.maxRetries) {
118
+ const delay = opts.baseDelayMs * Math.pow(2, attempt);
119
+ console.log(`[envoy-client] Transient error on attempt ${attempt + 1}/${opts.maxRetries + 1}: ${lastError.message}, retrying in ${delay}ms`);
120
+ serverWarn(`TRANSIENT-RETRY`, { url, error: lastError.message, attempt: attempt + 1, maxAttempts: opts.maxRetries + 1, delayMs: delay });
121
+ await sleep(delay);
122
+ continue;
123
+ }
124
+
125
+ throw lastError;
126
+ } finally {
127
+ clearTimeout(timeout);
128
+ }
129
+ }
130
+
131
+ throw lastError ?? new Error("Retry exhausted");
132
+ }
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // EnvoyClient — Command's interface to a remote Envoy
136
+ // ---------------------------------------------------------------------------
137
+
138
+ /**
139
+ * HTTP client for communicating with a Envoy agent.
140
+ *
141
+ * Command uses this to:
142
+ * 1. Check if the Envoy is healthy (pre-flight health check)
143
+ * 2. Delegate deployment execution to the Envoy
144
+ * 3. Query the Envoy's local state
145
+ */
146
+ export class EnvoyClient {
147
+ constructor(
148
+ private baseUrl: string,
149
+ private timeoutMs: number = 10_000,
150
+ ) {}
151
+
152
+ get url(): string { return this.baseUrl; }
153
+
154
+ /**
155
+ * Check Envoy health — used as the ServiceHealthChecker for the
156
+ * Command Agent's pre-flight health check step.
157
+ */
158
+ async checkHealth(): Promise<EnvoyHealthResponse> {
159
+ serverLog("ENVOY-HEALTH-CHECK", { url: this.baseUrl });
160
+ const start = Date.now();
161
+ const response = await fetchWithRetry(
162
+ `${this.baseUrl}/health`,
163
+ {},
164
+ this.timeoutMs,
165
+ );
166
+ if (!response.ok) {
167
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
168
+ }
169
+ const result = (await response.json()) as EnvoyHealthResponse;
170
+ serverLog("ENVOY-HEALTH-OK", { url: this.baseUrl, status: result.status, ready: result.readiness.ready, durationMs: Date.now() - start });
171
+ return result;
172
+ }
173
+
174
+ /**
175
+ * Send a deployment instruction to the Envoy and wait for the result.
176
+ */
177
+ async deploy(instruction: {
178
+ deploymentId: string;
179
+ partitionId: string;
180
+ environmentId: string;
181
+ operationId: string;
182
+ version: string;
183
+ variables: Record<string, string>;
184
+ environmentName: string;
185
+ partitionName: string;
186
+ progressCallbackUrl?: string;
187
+ }): Promise<EnvoyDeployResult> {
188
+ const response = await fetchWithRetry(
189
+ `${this.baseUrl}/deploy`,
190
+ {
191
+ method: "POST",
192
+ headers: { "Content-Type": "application/json" },
193
+ body: JSON.stringify(instruction),
194
+ },
195
+ this.timeoutMs * 3,
196
+ );
197
+
198
+ return (await response.json()) as EnvoyDeployResult;
199
+ }
200
+
201
+ /**
202
+ * Dispatch an approved plan to the Envoy for deterministic execution.
203
+ */
204
+ async executeApprovedPlan(params: {
205
+ deploymentId: string;
206
+ plan: DeploymentPlan;
207
+ rollbackPlan: DeploymentPlan;
208
+ artifactType: string;
209
+ artifactName: string;
210
+ environmentId: string;
211
+ progressCallbackUrl?: string;
212
+ callbackToken?: string;
213
+ }): Promise<EnvoyDeployResult> {
214
+ serverLog("ENVOY-EXECUTE", { deploymentId: params.deploymentId, envoyUrl: this.baseUrl, artifact: params.artifactName, steps: params.plan?.steps?.length ?? 0 });
215
+ const execStart = Date.now();
216
+ const response = await fetchWithRetry(
217
+ `${this.baseUrl}/execute`,
218
+ {
219
+ method: "POST",
220
+ headers: { "Content-Type": "application/json" },
221
+ body: JSON.stringify(params),
222
+ },
223
+ this.timeoutMs * 6, // execution may take longer
224
+ );
225
+
226
+ const execResult = (await response.json()) as EnvoyDeployResult;
227
+ if (execResult.success) {
228
+ serverLog("ENVOY-EXECUTE-COMPLETE", { deploymentId: params.deploymentId, durationMs: Date.now() - execStart });
229
+ } else {
230
+ serverLog("ENVOY-EXECUTE-FAILED", { deploymentId: params.deploymentId, failureReason: execResult.failureReason, durationMs: Date.now() - execStart });
231
+ }
232
+ return execResult;
233
+ }
234
+
235
+ /**
236
+ * Ask the Envoy to produce a deployment plan (read-only reasoning phase).
237
+ * Returns the plan and rollback plan; the server then submits them to
238
+ * POST /api/deployments/:id/plan to move the deployment to awaiting_approval.
239
+ */
240
+ async requestPlan(params: {
241
+ deploymentId: string;
242
+ artifact: {
243
+ id: string;
244
+ name: string;
245
+ type: string;
246
+ analysis: {
247
+ summary: string;
248
+ dependencies: string[];
249
+ configurationExpectations: Record<string, string>;
250
+ deploymentIntent?: string;
251
+ confidence: number;
252
+ };
253
+ };
254
+ environment: {
255
+ id: string;
256
+ name: string;
257
+ variables: Record<string, string>;
258
+ };
259
+ partition?: {
260
+ id: string;
261
+ name: string;
262
+ variables: Record<string, string>;
263
+ };
264
+ version: string;
265
+ resolvedVariables: Record<string, string>;
266
+ refinementFeedback?: string;
267
+ }): Promise<{ plan: DeploymentPlan; rollbackPlan: DeploymentPlan; delta?: string; assessmentSummary?: string; blocked?: boolean; blockReason?: string }> {
268
+ // Forward the LLM API key so the Envoy can use it if it started without one.
269
+ // Sent in the request body (not headers) over the trusted server↔envoy channel.
270
+ serverLog("ENVOY-PLAN-REQUEST", { deploymentId: params.deploymentId, envoyUrl: this.baseUrl, artifact: params.artifact.name, version: params.version, environment: params.environment.name });
271
+ const llmApiKey = process.env.SYNTH_LLM_API_KEY;
272
+ const planStart = Date.now();
273
+ const response = await fetchWithRetry(
274
+ `${this.baseUrl}/plan`,
275
+ {
276
+ method: "POST",
277
+ headers: { "Content-Type": "application/json" },
278
+ body: JSON.stringify(llmApiKey ? { ...params, llmApiKey } : params),
279
+ },
280
+ this.timeoutMs * 18, // planning may take time (probe loop + LLM reasoning)
281
+ );
282
+
283
+ if (!response.ok) {
284
+ const body = await response.text().catch(() => "");
285
+ serverLog("ENVOY-PLAN-FAILED", { deploymentId: params.deploymentId, status: response.status, durationMs: Date.now() - planStart });
286
+ throw new Error(`Envoy planning failed (HTTP ${response.status}): ${body}`);
287
+ }
288
+
289
+ const planResult = (await response.json()) as { plan: DeploymentPlan; rollbackPlan: DeploymentPlan; delta?: string; assessmentSummary?: string; blocked?: boolean; blockReason?: string };
290
+ serverLog("ENVOY-PLAN-RECEIVED", { deploymentId: params.deploymentId, steps: planResult.plan?.steps?.length ?? 0, blocked: planResult.blocked ?? false, durationMs: Date.now() - planStart });
291
+ return planResult;
292
+ }
293
+
294
+ /**
295
+ * Ask the Envoy to generate a rollback plan for a deployment that has
296
+ * already executed. Uses the execution record (what actually ran) rather
297
+ * than the forward plan, so the rollback is targeted to what actually changed.
298
+ */
299
+ async requestRollbackPlan(params: {
300
+ deploymentId: string;
301
+ artifact: {
302
+ name: string;
303
+ type: string;
304
+ analysis: {
305
+ summary: string;
306
+ dependencies: string[];
307
+ configurationExpectations: Record<string, string>;
308
+ deploymentIntent?: string;
309
+ confidence: number;
310
+ };
311
+ };
312
+ environment: { id: string; name: string };
313
+ completedSteps: Array<{
314
+ description: string;
315
+ action: string;
316
+ target: string;
317
+ status: "completed" | "failed" | "rolled_back";
318
+ output?: string;
319
+ }>;
320
+ deployedVariables: Record<string, string>;
321
+ version: string;
322
+ failureReason?: string;
323
+ }): Promise<DeploymentPlan> {
324
+ const llmApiKey = process.env.SYNTH_LLM_API_KEY;
325
+ const response = await fetchWithRetry(
326
+ `${this.baseUrl}/rollback-plan`,
327
+ {
328
+ method: "POST",
329
+ headers: { "Content-Type": "application/json" },
330
+ body: JSON.stringify(llmApiKey ? { ...params, llmApiKey } : params),
331
+ },
332
+ this.timeoutMs * 18, // LLM reasoning may take time
333
+ );
334
+
335
+ if (!response.ok) {
336
+ const body = await response.text().catch(() => "");
337
+ throw new Error(`Envoy rollback planning failed (HTTP ${response.status}): ${body}`);
338
+ }
339
+
340
+ const data = (await response.json()) as { rollbackPlan: DeploymentPlan };
341
+ return data.rollbackPlan;
342
+ }
343
+
344
+ /**
345
+ * Validate a modified plan against the Envoy's security boundaries.
346
+ */
347
+ async validatePlan(steps: PlannedStep[]): Promise<{ valid: boolean; violations: Array<{ step: string; reason: string }> }> {
348
+ const response = await fetchWithRetry(
349
+ `${this.baseUrl}/validate-plan`,
350
+ {
351
+ method: "POST",
352
+ headers: { "Content-Type": "application/json" },
353
+ body: JSON.stringify({ steps }),
354
+ },
355
+ this.timeoutMs,
356
+ );
357
+
358
+ return (await response.json()) as { valid: boolean; violations: Array<{ step: string; reason: string }> };
359
+ }
360
+
361
+ /**
362
+ * Ask the Envoy to validate whether user refinement feedback warrants a
363
+ * full replan. Cheap LLM call — no probe loop, no environment scanning.
364
+ * Always falls through to allow the replan if the call fails.
365
+ */
366
+ async validateRefinementFeedback(params: {
367
+ feedback: string;
368
+ currentPlanSteps: Array<{ description: string; action: string; target: string }>;
369
+ artifactName: string;
370
+ environmentName: string;
371
+ }): Promise<{ mode: "replan" | "rejection" | "response"; message: string }> {
372
+ const llmApiKey = process.env.SYNTH_LLM_API_KEY;
373
+ const response = await fetchWithRetry(
374
+ `${this.baseUrl}/validate-refinement`,
375
+ {
376
+ method: "POST",
377
+ headers: { "Content-Type": "application/json" },
378
+ body: JSON.stringify(llmApiKey ? { ...params, llmApiKey } : params),
379
+ },
380
+ this.timeoutMs * 3,
381
+ );
382
+ return (await response.json()) as { mode: "replan" | "rejection" | "response"; message: string };
383
+ }
384
+ }
385
+
386
+ // ---------------------------------------------------------------------------
387
+ // EnvoyHealthChecker — adapts EnvoyClient to ServiceHealthChecker
388
+ // ---------------------------------------------------------------------------
389
+
390
+ /**
391
+ * Adapts the EnvoyClient to the ServiceHealthChecker interface used by
392
+ * the Command Agent's pre-flight health check step.
393
+ *
394
+ * When a Envoy is registered for an environment, Command's health
395
+ * check actually reaches out to the Envoy instead of using the
396
+ * DefaultHealthChecker (which always returns healthy).
397
+ */
398
+ export class EnvoyHealthChecker implements ServiceHealthChecker {
399
+ private envoys = new Map<string, EnvoyClient>();
400
+
401
+ /**
402
+ * Register a Envoy for a specific environment.
403
+ * The serviceId format is "{operationId}/{environmentName}".
404
+ */
405
+ registerEnvoy(serviceId: string, client: EnvoyClient): void {
406
+ this.envoys.set(serviceId, client);
407
+ }
408
+
409
+ async check(
410
+ serviceId: string,
411
+ _context: { partitionId: string; environmentName: string },
412
+ ): Promise<HealthCheckResult> {
413
+ const client = this.envoys.get(serviceId);
414
+
415
+ if (!client) {
416
+ // No Envoy registered for this service — assume healthy
417
+ // (same as DefaultHealthChecker behavior)
418
+ return { reachable: true, responseTimeMs: 0, error: null };
419
+ }
420
+
421
+ serverLog("HEALTH-CHECK", { serviceId, url: client.url });
422
+ const start = Date.now();
423
+
424
+ try {
425
+ const health = await client.checkHealth();
426
+ const responseTimeMs = Date.now() - start;
427
+
428
+ if (health.status === "healthy" && health.readiness.ready) {
429
+ serverLog("HEALTH-OK", { serviceId, responseTimeMs });
430
+ return { reachable: true, responseTimeMs, error: null };
431
+ }
432
+
433
+ serverWarn("HEALTH-DEGRADED", { serviceId, responseTimeMs, status: health.status, reason: health.readiness.reason });
434
+ return {
435
+ reachable: false,
436
+ responseTimeMs,
437
+ error: `Envoy reports ${health.status}: ${health.readiness.reason}`,
438
+ };
439
+ } catch (err) {
440
+ const responseTimeMs = Date.now() - start;
441
+ const message =
442
+ err instanceof Error ? err.message : String(err);
443
+
444
+ // Map fetch errors to recognizable categories for the Command Agent
445
+ if (message.includes("abort")) {
446
+ serverError("HEALTH-TIMEOUT", { serviceId, responseTimeMs });
447
+ return {
448
+ reachable: false,
449
+ responseTimeMs,
450
+ error: `ETIMEDOUT: Envoy health check timed out after ${responseTimeMs}ms`,
451
+ };
452
+ }
453
+
454
+ if (
455
+ message.includes("ECONNREFUSED") ||
456
+ message.includes("fetch failed")
457
+ ) {
458
+ serverError("HEALTH-REFUSED", { serviceId, responseTimeMs, url: client.url });
459
+ return {
460
+ reachable: false,
461
+ responseTimeMs,
462
+ error: `ECONNREFUSED: Envoy at ${serviceId} is not responding`,
463
+ };
464
+ }
465
+
466
+ serverError("HEALTH-ERROR", { serviceId, responseTimeMs, error: message });
467
+ return {
468
+ reachable: false,
469
+ responseTimeMs,
470
+ error: `Envoy health check failed: ${message}`,
471
+ };
472
+ }
473
+ }
474
+ }