@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.
- package/dist/agent/debrief-retention.d.ts +12 -0
- package/dist/agent/debrief-retention.d.ts.map +1 -0
- package/dist/agent/debrief-retention.js +27 -0
- package/dist/agent/debrief-retention.js.map +1 -0
- package/dist/agent/envoy-client.d.ts +216 -0
- package/dist/agent/envoy-client.d.ts.map +1 -0
- package/dist/agent/envoy-client.js +266 -0
- package/dist/agent/envoy-client.js.map +1 -0
- package/dist/agent/envoy-registry.d.ts +102 -0
- package/dist/agent/envoy-registry.d.ts.map +1 -0
- package/dist/agent/envoy-registry.js +319 -0
- package/dist/agent/envoy-registry.js.map +1 -0
- package/dist/agent/health-checker.d.ts +39 -0
- package/dist/agent/health-checker.d.ts.map +1 -0
- package/dist/agent/health-checker.js +49 -0
- package/dist/agent/health-checker.js.map +1 -0
- package/dist/agent/mcp-client-manager.d.ts +36 -0
- package/dist/agent/mcp-client-manager.d.ts.map +1 -0
- package/dist/agent/mcp-client-manager.js +106 -0
- package/dist/agent/mcp-client-manager.js.map +1 -0
- package/dist/agent/stale-deployment-detector.d.ts +15 -0
- package/dist/agent/stale-deployment-detector.d.ts.map +1 -0
- package/dist/agent/stale-deployment-detector.js +50 -0
- package/dist/agent/stale-deployment-detector.js.map +1 -0
- package/dist/agent/step-runner.d.ts +31 -0
- package/dist/agent/step-runner.d.ts.map +1 -0
- package/dist/agent/step-runner.js +80 -0
- package/dist/agent/step-runner.js.map +1 -0
- package/dist/agent/synth-agent.d.ts +168 -0
- package/dist/agent/synth-agent.d.ts.map +1 -0
- package/dist/agent/synth-agent.js +1195 -0
- package/dist/agent/synth-agent.js.map +1 -0
- package/dist/api/agent.d.ts +36 -0
- package/dist/api/agent.d.ts.map +1 -0
- package/dist/api/agent.js +867 -0
- package/dist/api/agent.js.map +1 -0
- package/dist/api/api-keys.d.ts +4 -0
- package/dist/api/api-keys.d.ts.map +1 -0
- package/dist/api/api-keys.js +118 -0
- package/dist/api/api-keys.js.map +1 -0
- package/dist/api/artifacts.d.ts +5 -0
- package/dist/api/artifacts.d.ts.map +1 -0
- package/dist/api/artifacts.js +142 -0
- package/dist/api/artifacts.js.map +1 -0
- package/dist/api/auth.d.ts +4 -0
- package/dist/api/auth.d.ts.map +1 -0
- package/dist/api/auth.js +280 -0
- package/dist/api/auth.js.map +1 -0
- package/dist/api/deployments.d.ts +11 -0
- package/dist/api/deployments.d.ts.map +1 -0
- package/dist/api/deployments.js +1098 -0
- package/dist/api/deployments.js.map +1 -0
- package/dist/api/environments.d.ts +5 -0
- package/dist/api/environments.d.ts.map +1 -0
- package/dist/api/environments.js +69 -0
- package/dist/api/environments.js.map +1 -0
- package/dist/api/envoy-reports.d.ts +17 -0
- package/dist/api/envoy-reports.d.ts.map +1 -0
- package/dist/api/envoy-reports.js +138 -0
- package/dist/api/envoy-reports.js.map +1 -0
- package/dist/api/envoys.d.ts +5 -0
- package/dist/api/envoys.d.ts.map +1 -0
- package/dist/api/envoys.js +192 -0
- package/dist/api/envoys.js.map +1 -0
- package/dist/api/fleet.d.ts +11 -0
- package/dist/api/fleet.d.ts.map +1 -0
- package/dist/api/fleet.js +394 -0
- package/dist/api/fleet.js.map +1 -0
- package/dist/api/graph.d.ts +8 -0
- package/dist/api/graph.d.ts.map +1 -0
- package/dist/api/graph.js +355 -0
- package/dist/api/graph.js.map +1 -0
- package/dist/api/health.d.ts +20 -0
- package/dist/api/health.d.ts.map +1 -0
- package/dist/api/health.js +248 -0
- package/dist/api/health.js.map +1 -0
- package/dist/api/idp-schemas.d.ts +41 -0
- package/dist/api/idp-schemas.d.ts.map +1 -0
- package/dist/api/idp-schemas.js +17 -0
- package/dist/api/idp-schemas.js.map +1 -0
- package/dist/api/idp.d.ts +6 -0
- package/dist/api/idp.d.ts.map +1 -0
- package/dist/api/idp.js +620 -0
- package/dist/api/idp.js.map +1 -0
- package/dist/api/intake.d.ts +10 -0
- package/dist/api/intake.d.ts.map +1 -0
- package/dist/api/intake.js +418 -0
- package/dist/api/intake.js.map +1 -0
- package/dist/api/partitions.d.ts +5 -0
- package/dist/api/partitions.d.ts.map +1 -0
- package/dist/api/partitions.js +113 -0
- package/dist/api/partitions.js.map +1 -0
- package/dist/api/progress-event-store.d.ts +62 -0
- package/dist/api/progress-event-store.d.ts.map +1 -0
- package/dist/api/progress-event-store.js +118 -0
- package/dist/api/progress-event-store.js.map +1 -0
- package/dist/api/schemas.d.ts +1000 -0
- package/dist/api/schemas.d.ts.map +1 -0
- package/dist/api/schemas.js +328 -0
- package/dist/api/schemas.js.map +1 -0
- package/dist/api/security-boundaries.d.ts +4 -0
- package/dist/api/security-boundaries.d.ts.map +1 -0
- package/dist/api/security-boundaries.js +32 -0
- package/dist/api/security-boundaries.js.map +1 -0
- package/dist/api/settings.d.ts +4 -0
- package/dist/api/settings.d.ts.map +1 -0
- package/dist/api/settings.js +99 -0
- package/dist/api/settings.js.map +1 -0
- package/dist/api/system.d.ts +75 -0
- package/dist/api/system.d.ts.map +1 -0
- package/dist/api/system.js +558 -0
- package/dist/api/system.js.map +1 -0
- package/dist/api/telemetry.d.ts +4 -0
- package/dist/api/telemetry.d.ts.map +1 -0
- package/dist/api/telemetry.js +24 -0
- package/dist/api/telemetry.js.map +1 -0
- package/dist/api/users.d.ts +4 -0
- package/dist/api/users.d.ts.map +1 -0
- package/dist/api/users.js +173 -0
- package/dist/api/users.js.map +1 -0
- package/dist/archive-unpacker.d.ts +24 -0
- package/dist/archive-unpacker.d.ts.map +1 -0
- package/dist/archive-unpacker.js +239 -0
- package/dist/archive-unpacker.js.map +1 -0
- package/dist/artifact-analyzer.d.ts +59 -0
- package/dist/artifact-analyzer.d.ts.map +1 -0
- package/dist/artifact-analyzer.js +334 -0
- package/dist/artifact-analyzer.js.map +1 -0
- package/dist/auth/idp/index.d.ts +9 -0
- package/dist/auth/idp/index.d.ts.map +1 -0
- package/dist/auth/idp/index.js +5 -0
- package/dist/auth/idp/index.js.map +1 -0
- package/dist/auth/idp/ldap.d.ts +56 -0
- package/dist/auth/idp/ldap.d.ts.map +1 -0
- package/dist/auth/idp/ldap.js +276 -0
- package/dist/auth/idp/ldap.js.map +1 -0
- package/dist/auth/idp/oidc.d.ts +27 -0
- package/dist/auth/idp/oidc.d.ts.map +1 -0
- package/dist/auth/idp/oidc.js +97 -0
- package/dist/auth/idp/oidc.js.map +1 -0
- package/dist/auth/idp/role-mapping.d.ts +9 -0
- package/dist/auth/idp/role-mapping.d.ts.map +1 -0
- package/dist/auth/idp/role-mapping.js +16 -0
- package/dist/auth/idp/role-mapping.js.map +1 -0
- package/dist/auth/idp/saml.d.ts +40 -0
- package/dist/auth/idp/saml.d.ts.map +1 -0
- package/dist/auth/idp/saml.js +117 -0
- package/dist/auth/idp/saml.js.map +1 -0
- package/dist/auth/idp/types.d.ts +23 -0
- package/dist/auth/idp/types.d.ts.map +1 -0
- package/dist/auth/idp/types.js +2 -0
- package/dist/auth/idp/types.js.map +1 -0
- package/dist/fleet/fleet-executor.d.ts +35 -0
- package/dist/fleet/fleet-executor.d.ts.map +1 -0
- package/dist/fleet/fleet-executor.js +228 -0
- package/dist/fleet/fleet-executor.js.map +1 -0
- package/dist/fleet/fleet-store.d.ts +13 -0
- package/dist/fleet/fleet-store.d.ts.map +1 -0
- package/dist/fleet/fleet-store.js +13 -0
- package/dist/fleet/fleet-store.js.map +1 -0
- package/dist/fleet/index.d.ts +5 -0
- package/dist/fleet/index.d.ts.map +1 -0
- package/dist/fleet/index.js +4 -0
- package/dist/fleet/index.js.map +1 -0
- package/dist/fleet/representative-selector.d.ts +15 -0
- package/dist/fleet/representative-selector.d.ts.map +1 -0
- package/dist/fleet/representative-selector.js +71 -0
- package/dist/fleet/representative-selector.js.map +1 -0
- package/dist/graph/graph-executor.d.ts +36 -0
- package/dist/graph/graph-executor.d.ts.map +1 -0
- package/dist/graph/graph-executor.js +348 -0
- package/dist/graph/graph-executor.js.map +1 -0
- package/dist/graph/graph-inference.d.ts +22 -0
- package/dist/graph/graph-inference.d.ts.map +1 -0
- package/dist/graph/graph-inference.js +149 -0
- package/dist/graph/graph-inference.js.map +1 -0
- package/dist/graph/graph-store.d.ts +12 -0
- package/dist/graph/graph-store.d.ts.map +1 -0
- package/dist/graph/graph-store.js +61 -0
- package/dist/graph/graph-store.js.map +1 -0
- package/dist/graph/index.d.ts +5 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +4 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +837 -0
- package/dist/index.js.map +1 -0
- package/dist/intake/index.d.ts +6 -0
- package/dist/intake/index.d.ts.map +1 -0
- package/dist/intake/index.js +5 -0
- package/dist/intake/index.js.map +1 -0
- package/dist/intake/intake-processor.d.ts +17 -0
- package/dist/intake/intake-processor.d.ts.map +1 -0
- package/dist/intake/intake-processor.js +99 -0
- package/dist/intake/intake-processor.js.map +1 -0
- package/dist/intake/intake-store.d.ts +7 -0
- package/dist/intake/intake-store.d.ts.map +1 -0
- package/dist/intake/intake-store.js +7 -0
- package/dist/intake/intake-store.js.map +1 -0
- package/dist/intake/registry-poller.d.ts +41 -0
- package/dist/intake/registry-poller.d.ts.map +1 -0
- package/dist/intake/registry-poller.js +202 -0
- package/dist/intake/registry-poller.js.map +1 -0
- package/dist/intake/webhook-handlers.d.ts +37 -0
- package/dist/intake/webhook-handlers.d.ts.map +1 -0
- package/dist/intake/webhook-handlers.js +268 -0
- package/dist/intake/webhook-handlers.js.map +1 -0
- package/dist/logger.d.ts +5 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +15 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp/resources.d.ts +9 -0
- package/dist/mcp/resources.d.ts.map +1 -0
- package/dist/mcp/resources.js +72 -0
- package/dist/mcp/resources.js.map +1 -0
- package/dist/mcp/server.d.ts +15 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +20 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +88 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/middleware/auth.d.ts +29 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +76 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/permissions.d.ts +13 -0
- package/dist/middleware/permissions.d.ts.map +1 -0
- package/dist/middleware/permissions.js +32 -0
- package/dist/middleware/permissions.js.map +1 -0
- package/dist/pattern-store.d.ts +104 -0
- package/dist/pattern-store.d.ts.map +1 -0
- package/dist/pattern-store.js +299 -0
- package/dist/pattern-store.js.map +1 -0
- package/package.json +54 -0
- package/src/agent/debrief-retention.ts +44 -0
- package/src/agent/envoy-client.ts +474 -0
- package/src/agent/envoy-registry.ts +384 -0
- package/src/agent/health-checker.ts +70 -0
- package/src/agent/mcp-client-manager.ts +131 -0
- package/src/agent/stale-deployment-detector.ts +79 -0
- package/src/agent/step-runner.ts +124 -0
- package/src/agent/synth-agent.ts +1567 -0
- package/src/api/agent.ts +1075 -0
- package/src/api/api-keys.ts +129 -0
- package/src/api/artifacts.ts +194 -0
- package/src/api/auth.ts +320 -0
- package/src/api/deployments.ts +1347 -0
- package/src/api/environments.ts +97 -0
- package/src/api/envoy-reports.ts +159 -0
- package/src/api/envoys.ts +237 -0
- package/src/api/fleet.ts +510 -0
- package/src/api/graph.ts +516 -0
- package/src/api/health.ts +311 -0
- package/src/api/idp-schemas.ts +19 -0
- package/src/api/idp.ts +735 -0
- package/src/api/intake.ts +537 -0
- package/src/api/partitions.ts +147 -0
- package/src/api/progress-event-store.ts +153 -0
- package/src/api/schemas.ts +376 -0
- package/src/api/security-boundaries.ts +54 -0
- package/src/api/settings.ts +118 -0
- package/src/api/system.ts +704 -0
- package/src/api/telemetry.ts +32 -0
- package/src/api/users.ts +210 -0
- package/src/archive-unpacker.ts +271 -0
- package/src/artifact-analyzer.ts +438 -0
- package/src/auth/idp/index.ts +8 -0
- package/src/auth/idp/ldap.ts +340 -0
- package/src/auth/idp/oidc.ts +117 -0
- package/src/auth/idp/role-mapping.ts +22 -0
- package/src/auth/idp/saml.ts +148 -0
- package/src/auth/idp/types.ts +22 -0
- package/src/fleet/fleet-executor.ts +309 -0
- package/src/fleet/fleet-store.ts +13 -0
- package/src/fleet/index.ts +4 -0
- package/src/fleet/representative-selector.ts +83 -0
- package/src/graph/graph-executor.ts +446 -0
- package/src/graph/graph-inference.ts +184 -0
- package/src/graph/graph-store.ts +75 -0
- package/src/graph/index.ts +4 -0
- package/src/index.ts +916 -0
- package/src/intake/index.ts +5 -0
- package/src/intake/intake-processor.ts +111 -0
- package/src/intake/intake-store.ts +7 -0
- package/src/intake/registry-poller.ts +230 -0
- package/src/intake/webhook-handlers.ts +328 -0
- package/src/logger.ts +19 -0
- package/src/mcp/resources.ts +98 -0
- package/src/mcp/server.ts +34 -0
- package/src/mcp/tools.ts +117 -0
- package/src/middleware/auth.ts +103 -0
- package/src/middleware/permissions.ts +35 -0
- package/src/pattern-store.ts +409 -0
- package/tests/agent-mode.test.ts +536 -0
- package/tests/api-handlers.test.ts +1245 -0
- package/tests/archive-unpacker.test.ts +179 -0
- package/tests/artifact-analyzer.test.ts +240 -0
- package/tests/auth-middleware.test.ts +189 -0
- package/tests/decision-diary.test.ts +957 -0
- package/tests/diary-reader.test.ts +782 -0
- package/tests/envoy-client.test.ts +342 -0
- package/tests/envoy-reports.test.ts +156 -0
- package/tests/mcp-tools.test.ts +213 -0
- package/tests/orchestration.test.ts +536 -0
- package/tests/partition-deletion.test.ts +143 -0
- package/tests/partition-isolation.test.ts +830 -0
- package/tests/pattern-store.test.ts +371 -0
- package/tests/rbac-enforcement.test.ts +409 -0
- package/tests/ssrf-validation.test.ts +56 -0
- package/tests/stale-deployment.test.ts +85 -0
- package/tests/step-runner.test.ts +308 -0
- package/tests/ui-journey.test.ts +330 -0
- package/tsconfig.json +11 -0
- package/vitest.config.ts +27 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import Fastify from "fastify";
|
|
6
|
+
import fastifyCors from "@fastify/cors";
|
|
7
|
+
import rateLimit from "@fastify/rate-limit";
|
|
8
|
+
import fastifyStatic from "@fastify/static";
|
|
9
|
+
import fastifyFormBody from "@fastify/formbody";
|
|
10
|
+
import fastifyMultipart from "@fastify/multipart";
|
|
11
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
12
|
+
import { PersistentDecisionDebrief, openEntityDatabase, PersistentPartitionStore, PersistentEnvironmentStore, PersistentSettingsStore, PersistentDeploymentStore, PersistentArtifactStore, PersistentSecurityBoundaryStore, PersistentTelemetryStore, PersistentUserStore, PersistentRoleStore, PersistentUserRoleStore, PersistentSessionStore, PersistentIdpProviderStore, PersistentRoleMappingStore, PersistentApiKeyStore, PersistentEnvoyRegistryStore, PersistentRegistryPollerVersionStore, LlmClient, buildLlmConfigFromSettings, initEdition, EditionError } from "@synth-deploy/core";
|
|
13
|
+
import { SynthAgent } from "./agent/synth-agent.js";
|
|
14
|
+
import { EnvoyHealthChecker } from "./agent/health-checker.js";
|
|
15
|
+
import { McpClientManager } from "./agent/mcp-client-manager.js";
|
|
16
|
+
import { createMcpServer } from "./mcp/server.js";
|
|
17
|
+
import { registerDeploymentRoutes } from "./api/deployments.js";
|
|
18
|
+
import { registerHealthRoutes } from "./api/health.js";
|
|
19
|
+
import { registerEnvoyReportRoutes } from "./api/envoy-reports.js";
|
|
20
|
+
import { registerArtifactRoutes } from "./api/artifacts.js";
|
|
21
|
+
import { registerSecurityBoundaryRoutes } from "./api/security-boundaries.js";
|
|
22
|
+
import { registerPartitionRoutes } from "./api/partitions.js";
|
|
23
|
+
import { registerEnvironmentRoutes } from "./api/environments.js";
|
|
24
|
+
import { registerAgentRoutes } from "./api/agent.js";
|
|
25
|
+
import { registerSettingsRoutes } from "./api/settings.js";
|
|
26
|
+
import { registerTelemetryRoutes } from "./api/telemetry.js";
|
|
27
|
+
import { registerEnvoyRoutes } from "./api/envoys.js";
|
|
28
|
+
import { EnvoyRegistry } from "./agent/envoy-registry.js";
|
|
29
|
+
import { EnvoyClient } from "./agent/envoy-client.js";
|
|
30
|
+
import { registerSystemRoutes } from "./api/system.js";
|
|
31
|
+
import { registerAuthMiddleware } from "./middleware/auth.js";
|
|
32
|
+
import { registerAuthRoutes } from "./api/auth.js";
|
|
33
|
+
import { registerApiKeyRoutes } from "./api/api-keys.js";
|
|
34
|
+
import { registerUserRoutes } from "./api/users.js";
|
|
35
|
+
import { registerIdpRoutes } from "./api/idp.js";
|
|
36
|
+
import { startStaleDeploymentScanner } from "./agent/stale-deployment-detector.js";
|
|
37
|
+
import { startRetentionScanner } from "./agent/debrief-retention.js";
|
|
38
|
+
import { ProgressEventStore } from "./api/progress-event-store.js";
|
|
39
|
+
import { registerFleetRoutes } from "./api/fleet.js";
|
|
40
|
+
import { FleetDeploymentStore, FleetExecutor } from "./fleet/index.js";
|
|
41
|
+
import { IntakeChannelStore, IntakeEventStore, IntakeProcessor, RegistryPoller } from "./intake/index.js";
|
|
42
|
+
import { registerIntakeRoutes } from "./api/intake.js";
|
|
43
|
+
import { ArtifactAnalyzer } from "./artifact-analyzer.js";
|
|
44
|
+
import { DeploymentGraphStore, GraphInferenceEngine } from "./graph/index.js";
|
|
45
|
+
import { registerGraphRoutes } from "./api/graph.js";
|
|
46
|
+
import { initServerLogger } from "./logger.js";
|
|
47
|
+
// --- Bootstrap shared state ---
|
|
48
|
+
// Default to repo-root/data, resolved relative to this file so it's consistent
|
|
49
|
+
// regardless of the working directory when the server is started.
|
|
50
|
+
const __serverDir = path.dirname(fileURLToPath(import.meta.url));
|
|
51
|
+
const DATA_DIR = process.env.SYNTH_DATA_DIR
|
|
52
|
+
? path.resolve(process.env.SYNTH_DATA_DIR)
|
|
53
|
+
: path.resolve(__serverDir, "../../../data");
|
|
54
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
55
|
+
chmodSync(DATA_DIR, 0o700);
|
|
56
|
+
initServerLogger(DATA_DIR);
|
|
57
|
+
// --- JWT secret — auto-generated on first run, persisted to disk ---
|
|
58
|
+
// SYNTH_JWT_SECRET env var takes precedence (for CI, multi-instance, key rotation).
|
|
59
|
+
// When absent, a secret is generated once and stored at DATA_DIR/jwt-secret (mode 0600).
|
|
60
|
+
const JWT_SECRET_FILE = path.join(DATA_DIR, "jwt-secret");
|
|
61
|
+
function resolveJwtSecret() {
|
|
62
|
+
if (process.env.SYNTH_JWT_SECRET)
|
|
63
|
+
return process.env.SYNTH_JWT_SECRET;
|
|
64
|
+
if (existsSync(JWT_SECRET_FILE))
|
|
65
|
+
return readFileSync(JWT_SECRET_FILE, "utf-8").trim();
|
|
66
|
+
const generated = crypto.randomBytes(32).toString("hex");
|
|
67
|
+
writeFileSync(JWT_SECRET_FILE, generated, { mode: 0o600 });
|
|
68
|
+
console.log(`[Synth] Generated JWT secret — stored at ${JWT_SECRET_FILE}`);
|
|
69
|
+
return generated;
|
|
70
|
+
}
|
|
71
|
+
const resolvedJwtSecret = resolveJwtSecret();
|
|
72
|
+
const debrief = new PersistentDecisionDebrief(path.join(DATA_DIR, "debrief.db"));
|
|
73
|
+
const entityDb = openEntityDatabase(path.join(DATA_DIR, "synth.db"));
|
|
74
|
+
const hasDedicatedEncryptionKey = !!process.env.SYNTH_ENCRYPTION_KEY;
|
|
75
|
+
const idpEncryptionSecret = process.env.SYNTH_ENCRYPTION_KEY ?? resolvedJwtSecret;
|
|
76
|
+
const partitions = new PersistentPartitionStore(entityDb);
|
|
77
|
+
const environments = new PersistentEnvironmentStore(entityDb);
|
|
78
|
+
const settings = new PersistentSettingsStore(entityDb, idpEncryptionSecret);
|
|
79
|
+
const deployments = new PersistentDeploymentStore(entityDb);
|
|
80
|
+
const artifactStore = new PersistentArtifactStore(entityDb);
|
|
81
|
+
const securityBoundaryStore = new PersistentSecurityBoundaryStore(entityDb);
|
|
82
|
+
const telemetryStore = new PersistentTelemetryStore(entityDb);
|
|
83
|
+
const userStore = new PersistentUserStore(entityDb);
|
|
84
|
+
const roleStore = new PersistentRoleStore(entityDb);
|
|
85
|
+
const userRoleStore = new PersistentUserRoleStore(entityDb, roleStore);
|
|
86
|
+
const sessionStore = new PersistentSessionStore(entityDb);
|
|
87
|
+
const apiKeyStore = new PersistentApiKeyStore(entityDb);
|
|
88
|
+
const idpProviderStore = new PersistentIdpProviderStore(entityDb, idpEncryptionSecret);
|
|
89
|
+
const roleMappingStore = new PersistentRoleMappingStore(entityDb);
|
|
90
|
+
// Load persisted LLM API key into env if not already set via environment
|
|
91
|
+
if (!process.env.SYNTH_LLM_API_KEY) {
|
|
92
|
+
const storedKey = settings.getSecret("llm_api_key");
|
|
93
|
+
if (storedKey)
|
|
94
|
+
process.env.SYNTH_LLM_API_KEY = storedKey;
|
|
95
|
+
}
|
|
96
|
+
// Require SYNTH_ENCRYPTION_KEY when IdP providers are configured
|
|
97
|
+
if (!hasDedicatedEncryptionKey && idpProviderStore.list().length > 0) {
|
|
98
|
+
console.error("[Synth] FATAL: SYNTH_ENCRYPTION_KEY is not set but IdP providers are configured. " +
|
|
99
|
+
"A dedicated encryption key is required to protect IdP client secrets at rest. " +
|
|
100
|
+
"Set SYNTH_ENCRYPTION_KEY to a dedicated secret separate from SYNTH_JWT_SECRET. Exiting.");
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
// Startup validation — log warnings for missing optional env vars
|
|
104
|
+
if (!process.env.SYNTH_LLM_API_KEY) {
|
|
105
|
+
console.warn("[Synth] WARNING: SYNTH_LLM_API_KEY is not set. LLM features will be unavailable until configured via settings.");
|
|
106
|
+
}
|
|
107
|
+
if (!process.env.SYNTH_DATA_DIR) {
|
|
108
|
+
console.warn("[Synth] WARNING: SYNTH_DATA_DIR is not set. Using default ./data directory.");
|
|
109
|
+
}
|
|
110
|
+
// --- Resolve edition (Community or Enterprise) ---
|
|
111
|
+
initEdition();
|
|
112
|
+
const envoyRegistryStore = new PersistentEnvoyRegistryStore(entityDb);
|
|
113
|
+
const envoyRegistry = new EnvoyRegistry(envoyRegistryStore);
|
|
114
|
+
const envoyUrl = process.env.SYNTH_ENVOY_URL ?? "http://localhost:9411";
|
|
115
|
+
const jwtSecret = new TextEncoder().encode(resolvedJwtSecret);
|
|
116
|
+
// --- Seed default roles ---
|
|
117
|
+
const ALL_PERMISSIONS = [
|
|
118
|
+
"deployment.create", "deployment.approve", "deployment.reject", "deployment.view", "deployment.rollback",
|
|
119
|
+
"artifact.create", "artifact.update", "artifact.annotate", "artifact.delete", "artifact.view",
|
|
120
|
+
"environment.create", "environment.update", "environment.delete", "environment.view",
|
|
121
|
+
"partition.create", "partition.update", "partition.delete", "partition.view",
|
|
122
|
+
"envoy.register", "envoy.configure", "envoy.view",
|
|
123
|
+
"settings.manage", "users.manage", "roles.manage",
|
|
124
|
+
];
|
|
125
|
+
const DEPLOYER_PERMISSIONS = [
|
|
126
|
+
"deployment.create", "deployment.approve", "deployment.reject", "deployment.view", "deployment.rollback",
|
|
127
|
+
"artifact.create", "artifact.update", "artifact.annotate", "artifact.view",
|
|
128
|
+
"environment.view",
|
|
129
|
+
"partition.view",
|
|
130
|
+
"envoy.view",
|
|
131
|
+
];
|
|
132
|
+
const VIEWER_PERMISSIONS = [
|
|
133
|
+
"deployment.view",
|
|
134
|
+
"artifact.view",
|
|
135
|
+
"environment.view",
|
|
136
|
+
"partition.view",
|
|
137
|
+
"envoy.view",
|
|
138
|
+
];
|
|
139
|
+
if (roleStore.list().length === 0) {
|
|
140
|
+
roleStore.create({
|
|
141
|
+
id: crypto.randomUUID(),
|
|
142
|
+
name: "Admin",
|
|
143
|
+
permissions: ALL_PERMISSIONS,
|
|
144
|
+
isBuiltIn: true,
|
|
145
|
+
createdAt: new Date(),
|
|
146
|
+
});
|
|
147
|
+
roleStore.create({
|
|
148
|
+
id: crypto.randomUUID(),
|
|
149
|
+
name: "Deployer",
|
|
150
|
+
permissions: DEPLOYER_PERMISSIONS,
|
|
151
|
+
isBuiltIn: true,
|
|
152
|
+
createdAt: new Date(),
|
|
153
|
+
});
|
|
154
|
+
roleStore.create({
|
|
155
|
+
id: crypto.randomUUID(),
|
|
156
|
+
name: "Viewer",
|
|
157
|
+
permissions: VIEWER_PERMISSIONS,
|
|
158
|
+
isBuiltIn: true,
|
|
159
|
+
createdAt: new Date(),
|
|
160
|
+
});
|
|
161
|
+
console.log("[Synth] Seeded default roles: Admin, Deployer, Viewer");
|
|
162
|
+
}
|
|
163
|
+
const healthCheckerUrl = settings.get().envoy?.url;
|
|
164
|
+
const healthChecker = healthCheckerUrl ? new EnvoyHealthChecker(healthCheckerUrl) : undefined;
|
|
165
|
+
const agent = new SynthAgent(debrief, deployments, artifactStore, environments, partitions, healthChecker, {}, settings);
|
|
166
|
+
// Initialize server LLM client — picks up provider from env or settings
|
|
167
|
+
const llmSettings = settings.get().llm;
|
|
168
|
+
const llm = new LlmClient(debrief, "server", buildLlmConfigFromSettings(llmSettings));
|
|
169
|
+
if (llm.isAvailable()) {
|
|
170
|
+
console.log(`[Synth] LLM available for artifact analysis (provider: ${llmSettings?.provider ?? "anthropic"})`);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
console.warn("[Synth] LLM not available for artifact analysis — set SYNTH_LLM_API_KEY or configure via Settings");
|
|
174
|
+
}
|
|
175
|
+
const artifactAnalyzer = new ArtifactAnalyzer({ llm, debrief });
|
|
176
|
+
// --- Connect to external MCP servers (if configured) ---
|
|
177
|
+
const mcpClientManager = new McpClientManager();
|
|
178
|
+
const mcpServerConfigs = settings.get().mcpServers ?? [];
|
|
179
|
+
if (mcpServerConfigs.length > 0) {
|
|
180
|
+
await mcpClientManager.connectAll(mcpServerConfigs);
|
|
181
|
+
const connected = mcpClientManager.getConnectedServers();
|
|
182
|
+
if (connected.length > 0) {
|
|
183
|
+
console.log(`[MCP Client] Connected to ${connected.length} external server(s): ${connected.join(", ")}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
agent.mcpClientManager = mcpClientManager;
|
|
187
|
+
// --- Seed demo data so the server is immediately usable ---
|
|
188
|
+
if (process.env.SYNTH_SEED_DEMO !== 'false' && partitions.list().length === 0) {
|
|
189
|
+
function hoursAgo(h) { return new Date(Date.now() - h * 3600_000); }
|
|
190
|
+
// Environments
|
|
191
|
+
const prodEnv = environments.create("production", { APP_ENV: "production", LOG_LEVEL: "warn" });
|
|
192
|
+
const stagingEnv = environments.create("staging", { APP_ENV: "staging", LOG_LEVEL: "debug" });
|
|
193
|
+
const devEnv = environments.create("development", { APP_ENV: "development", LOG_LEVEL: "trace" });
|
|
194
|
+
// Partitions
|
|
195
|
+
const acmePartition = partitions.create("Acme Corp", { APP_ENV: "production", DB_HOST: "acme-db-1", REGION: "us-east-1" });
|
|
196
|
+
const globexPartition = partitions.create("Globex Industries", { APP_ENV: "production", DB_HOST: "globex-db-1", REGION: "eu-west-1" });
|
|
197
|
+
const initechPartition = partitions.create("Initech", { APP_ENV: "production", DB_HOST: "initech-db-1", REGION: "us-west-2" });
|
|
198
|
+
// --- Artifacts with analysis, versions, and annotations ---
|
|
199
|
+
const webAppArtifact = artifactStore.create({
|
|
200
|
+
name: "web-app",
|
|
201
|
+
type: "nodejs",
|
|
202
|
+
analysis: {
|
|
203
|
+
summary: "Node.js web application with Express backend and React frontend. Requires PostgreSQL and Redis.",
|
|
204
|
+
dependencies: ["postgresql", "redis", "node:20"],
|
|
205
|
+
configurationExpectations: { DB_HOST: "PostgreSQL hostname", REDIS_URL: "Redis connection string", APP_ENV: "Runtime environment" },
|
|
206
|
+
deploymentIntent: "Rolling deployment with zero-downtime via health check gating",
|
|
207
|
+
confidence: 0.92,
|
|
208
|
+
},
|
|
209
|
+
annotations: [
|
|
210
|
+
{ field: "dependencies", correction: "Added redis dependency — missed in initial analysis", annotatedBy: "operator", annotatedAt: hoursAgo(48) },
|
|
211
|
+
],
|
|
212
|
+
learningHistory: [
|
|
213
|
+
{ timestamp: hoursAgo(96), event: "initial-analysis", details: "First artifact analysis completed from Dockerfile and package.json" },
|
|
214
|
+
{ timestamp: hoursAgo(48), event: "annotation-applied", details: "Operator corrected missing redis dependency" },
|
|
215
|
+
{ timestamp: hoursAgo(24), event: "reanalysis", details: "Re-analyzed after v2.4.1 deployment — confidence improved from 0.85 to 0.92" },
|
|
216
|
+
],
|
|
217
|
+
});
|
|
218
|
+
const apiArtifact = artifactStore.create({
|
|
219
|
+
name: "api-service",
|
|
220
|
+
type: "docker",
|
|
221
|
+
analysis: {
|
|
222
|
+
summary: "Containerized REST API service. Stateless, scales horizontally. Requires connection to shared PostgreSQL.",
|
|
223
|
+
dependencies: ["postgresql", "docker-runtime"],
|
|
224
|
+
configurationExpectations: { API_URL: "Service endpoint URL", DB_HOST: "PostgreSQL hostname" },
|
|
225
|
+
deploymentIntent: "Blue-green deployment with endpoint health verification",
|
|
226
|
+
confidence: 0.88,
|
|
227
|
+
},
|
|
228
|
+
annotations: [],
|
|
229
|
+
learningHistory: [
|
|
230
|
+
{ timestamp: hoursAgo(72), event: "initial-analysis", details: "Analyzed from Dockerfile and docker-compose.yml" },
|
|
231
|
+
{ timestamp: hoursAgo(36), event: "failure-learning", details: "v1.11.0 failed due to port conflict — added pre-deploy cleanup recommendation" },
|
|
232
|
+
],
|
|
233
|
+
});
|
|
234
|
+
const workerArtifact = artifactStore.create({
|
|
235
|
+
name: "worker-service",
|
|
236
|
+
type: "binary",
|
|
237
|
+
analysis: {
|
|
238
|
+
summary: "Compiled Go binary for background job processing. Reads from RabbitMQ, writes to PostgreSQL.",
|
|
239
|
+
dependencies: ["rabbitmq", "postgresql"],
|
|
240
|
+
configurationExpectations: { QUEUE_URL: "RabbitMQ connection string", DB_HOST: "PostgreSQL hostname" },
|
|
241
|
+
deploymentIntent: "Stop-deploy-start with queue depth verification",
|
|
242
|
+
confidence: 0.95,
|
|
243
|
+
},
|
|
244
|
+
annotations: [
|
|
245
|
+
{ field: "deploymentIntent", correction: "Changed from rolling to stop-deploy-start — workers must fully drain before restart", annotatedBy: "operator", annotatedAt: hoursAgo(20) },
|
|
246
|
+
],
|
|
247
|
+
learningHistory: [
|
|
248
|
+
{ timestamp: hoursAgo(80), event: "initial-analysis", details: "Analyzed from Makefile and systemd unit file" },
|
|
249
|
+
{ timestamp: hoursAgo(20), event: "annotation-applied", details: "Operator corrected deployment strategy to stop-deploy-start" },
|
|
250
|
+
{ timestamp: hoursAgo(3), event: "successful-deployment", details: "v3.0.0 deployed successfully with corrected strategy" },
|
|
251
|
+
],
|
|
252
|
+
});
|
|
253
|
+
// Artifact versions
|
|
254
|
+
artifactStore.addVersion({ artifactId: webAppArtifact.id, version: "2.3.0", source: "npm-registry", metadata: { commit: "abc1234", builtBy: "ci" } });
|
|
255
|
+
artifactStore.addVersion({ artifactId: webAppArtifact.id, version: "2.4.0", source: "npm-registry", metadata: { commit: "def5678", builtBy: "ci" } });
|
|
256
|
+
artifactStore.addVersion({ artifactId: webAppArtifact.id, version: "2.4.1", source: "npm-registry", metadata: { commit: "ghi9012", builtBy: "ci", hotfix: "true" } });
|
|
257
|
+
artifactStore.addVersion({ artifactId: webAppArtifact.id, version: "2.5.0-rc.1", source: "npm-registry", metadata: { commit: "jkl3456", builtBy: "ci", prerelease: "true" } });
|
|
258
|
+
artifactStore.addVersion({ artifactId: apiArtifact.id, version: "1.11.0", source: "docker-registry", metadata: { image: "api-service:1.11.0", digest: "sha256:a4f8e" } });
|
|
259
|
+
artifactStore.addVersion({ artifactId: apiArtifact.id, version: "1.12.0", source: "docker-registry", metadata: { image: "api-service:1.12.0", digest: "sha256:b5c9f" } });
|
|
260
|
+
artifactStore.addVersion({ artifactId: apiArtifact.id, version: "1.13.0-beta.2", source: "docker-registry", metadata: { image: "api-service:1.13.0-beta.2", digest: "sha256:c6d0a", prerelease: "true" } });
|
|
261
|
+
artifactStore.addVersion({ artifactId: workerArtifact.id, version: "2.9.0", source: "github-releases", metadata: { commit: "mno7890", binary: "worker-linux-amd64" } });
|
|
262
|
+
artifactStore.addVersion({ artifactId: workerArtifact.id, version: "3.0.0", source: "github-releases", metadata: { commit: "pqr1234", binary: "worker-linux-amd64", majorUpgrade: "true" } });
|
|
263
|
+
// --- Deployments (mix of statuses and ages) ---
|
|
264
|
+
const dep1 = {
|
|
265
|
+
id: crypto.randomUUID(), artifactId: webAppArtifact.id, partitionId: acmePartition.id,
|
|
266
|
+
environmentId: prodEnv.id, version: "2.3.0", status: "succeeded",
|
|
267
|
+
variables: { ...acmePartition.variables, ...prodEnv.variables },
|
|
268
|
+
plan: {
|
|
269
|
+
steps: [
|
|
270
|
+
{ description: "Stop service", action: "systemctl stop web-app", target: "prd-web-01", reversible: true, rollbackAction: "systemctl start web-app", execPreview: "systemctl stop web-app" },
|
|
271
|
+
{ description: "Backup current binaries", action: "cp -r /opt/web-app/ /opt/web-app.bak/", target: "prd-web-01", reversible: false, execPreview: "cp -r /opt/web-app/ /opt/web-app.bak/" },
|
|
272
|
+
{ description: "Deploy new artifact", action: "tar -xzf web-app-2.3.0.tar.gz -C /opt/web-app/", target: "prd-web-01", reversible: true, rollbackAction: "cp -r /opt/web-app.bak/ /opt/web-app/", execPreview: "tar -xzf /opt/releases/web-app-2.3.0.tar.gz -C /opt/web-app/" },
|
|
273
|
+
{ description: "Apply environment config (1 variable changed: API_ENDPOINT)", action: "envsubst < config.template > /opt/web-app/.env", target: "prd-web-01", reversible: true, rollbackAction: "cp /opt/web-app.bak/.env /opt/web-app/.env", execPreview: "envsubst < /opt/web-app/config.template > /opt/web-app/.env" },
|
|
274
|
+
{ description: "Start service and verify health endpoint → 200 OK", action: "systemctl start web-app && curl -f http://localhost:8080/health", target: "prd-web-01", reversible: true, rollbackAction: "systemctl stop web-app", execPreview: "systemctl start web-app" },
|
|
275
|
+
],
|
|
276
|
+
reasoning: "Standard 5-step deploy: stop, backup, extract, config, start. One config change: API_ENDPOINT updated to v2 endpoint validated in staging for 4h.",
|
|
277
|
+
diffFromCurrent: [
|
|
278
|
+
{ key: "API_ENDPOINT", from: "https://api.acme.corp/v1", to: "https://api.acme.corp/v2" },
|
|
279
|
+
],
|
|
280
|
+
},
|
|
281
|
+
debriefEntryIds: [],
|
|
282
|
+
createdAt: hoursAgo(72), completedAt: hoursAgo(71.5), failureReason: undefined,
|
|
283
|
+
};
|
|
284
|
+
const dep2 = {
|
|
285
|
+
id: crypto.randomUUID(), artifactId: webAppArtifact.id, partitionId: acmePartition.id,
|
|
286
|
+
environmentId: prodEnv.id, version: "2.4.0", status: "succeeded",
|
|
287
|
+
variables: { ...acmePartition.variables, ...prodEnv.variables },
|
|
288
|
+
debriefEntryIds: [],
|
|
289
|
+
createdAt: hoursAgo(48), completedAt: hoursAgo(47.8), failureReason: undefined,
|
|
290
|
+
};
|
|
291
|
+
const dep3 = {
|
|
292
|
+
id: crypto.randomUUID(), artifactId: webAppArtifact.id, partitionId: acmePartition.id,
|
|
293
|
+
environmentId: prodEnv.id, version: "2.4.1", status: "succeeded",
|
|
294
|
+
variables: { ...acmePartition.variables, ...prodEnv.variables },
|
|
295
|
+
debriefEntryIds: [],
|
|
296
|
+
createdAt: hoursAgo(24), completedAt: hoursAgo(23.7), failureReason: undefined,
|
|
297
|
+
};
|
|
298
|
+
const dep4 = {
|
|
299
|
+
id: crypto.randomUUID(), artifactId: apiArtifact.id, partitionId: acmePartition.id,
|
|
300
|
+
environmentId: prodEnv.id, version: "1.11.0", status: "failed",
|
|
301
|
+
variables: { ...acmePartition.variables, ...prodEnv.variables },
|
|
302
|
+
debriefEntryIds: [],
|
|
303
|
+
createdAt: hoursAgo(36), completedAt: hoursAgo(35.9),
|
|
304
|
+
failureReason: "Health check failed after 3 retries: connection refused on port 8080",
|
|
305
|
+
};
|
|
306
|
+
const dep5 = {
|
|
307
|
+
id: crypto.randomUUID(), artifactId: apiArtifact.id, partitionId: acmePartition.id,
|
|
308
|
+
environmentId: prodEnv.id, version: "1.12.0", status: "succeeded",
|
|
309
|
+
variables: { ...acmePartition.variables, ...prodEnv.variables },
|
|
310
|
+
debriefEntryIds: [],
|
|
311
|
+
createdAt: hoursAgo(12), completedAt: hoursAgo(11.8), failureReason: undefined,
|
|
312
|
+
};
|
|
313
|
+
const dep6 = {
|
|
314
|
+
id: crypto.randomUUID(), artifactId: webAppArtifact.id, partitionId: globexPartition.id,
|
|
315
|
+
environmentId: stagingEnv.id, version: "2.5.0-rc.1", status: "succeeded",
|
|
316
|
+
variables: { ...globexPartition.variables, ...stagingEnv.variables },
|
|
317
|
+
debriefEntryIds: [],
|
|
318
|
+
createdAt: hoursAgo(6), completedAt: hoursAgo(5.8), failureReason: undefined,
|
|
319
|
+
};
|
|
320
|
+
const dep7 = {
|
|
321
|
+
id: crypto.randomUUID(), artifactId: workerArtifact.id, partitionId: initechPartition.id,
|
|
322
|
+
environmentId: prodEnv.id, version: "2.9.0", status: "failed",
|
|
323
|
+
variables: { ...initechPartition.variables, ...prodEnv.variables },
|
|
324
|
+
debriefEntryIds: [],
|
|
325
|
+
createdAt: hoursAgo(18), completedAt: hoursAgo(17.8),
|
|
326
|
+
failureReason: "Queue depth exceeded threshold (342 > 100) during verification",
|
|
327
|
+
};
|
|
328
|
+
const dep8 = {
|
|
329
|
+
id: crypto.randomUUID(), artifactId: workerArtifact.id, partitionId: initechPartition.id,
|
|
330
|
+
environmentId: prodEnv.id, version: "3.0.0", status: "succeeded",
|
|
331
|
+
variables: { ...initechPartition.variables, ...prodEnv.variables },
|
|
332
|
+
debriefEntryIds: [],
|
|
333
|
+
createdAt: hoursAgo(3), completedAt: hoursAgo(2.7), failureReason: undefined,
|
|
334
|
+
};
|
|
335
|
+
const dep9 = {
|
|
336
|
+
id: crypto.randomUUID(), artifactId: apiArtifact.id, partitionId: globexPartition.id,
|
|
337
|
+
environmentId: stagingEnv.id, version: "1.13.0-beta.2", status: "running",
|
|
338
|
+
variables: { ...globexPartition.variables, ...stagingEnv.variables },
|
|
339
|
+
plan: {
|
|
340
|
+
steps: [
|
|
341
|
+
{ description: "Pull latest image from registry", action: "docker pull", target: "registry.internal/api:1.13.0-beta.2", reversible: true, rollbackAction: "docker pull registry.internal/api:1.12.0", execPreview: "docker pull registry.internal/api:1.13.0-beta.2" },
|
|
342
|
+
{ description: "Stop running container", action: "docker stop", target: "api-staging", reversible: true, rollbackAction: "docker start api-staging", execPreview: "docker stop api-staging" },
|
|
343
|
+
{ description: "Start new container with updated image", action: "docker run", target: "registry.internal/api:1.13.0-beta.2", reversible: true, rollbackAction: "docker stop api-staging && docker run ... api:1.12.0", execPreview: "docker run -d --name api-staging --env-file /opt/api/.env -p 8080:8080 registry.internal/api:1.13.0-beta.2" },
|
|
344
|
+
{ description: "Verify health endpoint returns 200", action: "verify health", target: "http://localhost:8080/health", reversible: false, execPreview: "curl -f --retry 3 --retry-delay 5 http://localhost:8080/health" },
|
|
345
|
+
],
|
|
346
|
+
reasoning: "Container swap: pull new image, stop old container, start new one, verify health. Staging environment — rollback is fast via image tag swap.",
|
|
347
|
+
},
|
|
348
|
+
debriefEntryIds: [],
|
|
349
|
+
createdAt: hoursAgo(0.5),
|
|
350
|
+
};
|
|
351
|
+
const dep11 = {
|
|
352
|
+
id: crypto.randomUUID(), artifactId: workerArtifact.id, partitionId: globexPartition.id,
|
|
353
|
+
environmentId: prodEnv.id, version: "3.1.0", status: "awaiting_approval",
|
|
354
|
+
variables: { ...globexPartition.variables, ...prodEnv.variables },
|
|
355
|
+
plan: {
|
|
356
|
+
steps: [
|
|
357
|
+
{ description: "Drain queue — wait for in-flight jobs to complete", action: "run command", target: "worker-drain", reversible: false, execPreview: "npm run worker:drain --timeout=120" },
|
|
358
|
+
{ description: "Stop worker processes on all nodes", action: "systemctl stop", target: "synth-worker", reversible: true, rollbackAction: "systemctl start synth-worker", execPreview: "systemctl stop synth-worker" },
|
|
359
|
+
{ description: "Deploy new worker binary", action: "copy file", target: "/opt/worker/", reversible: true, rollbackAction: "restore /opt/worker/ from backup", execPreview: "cp -r /opt/releases/worker-3.1.0/* /opt/worker/" },
|
|
360
|
+
{ description: "Update queue concurrency config (WORKER_CONCURRENCY: 4 → 8)", action: "write config", target: "/opt/worker/.env", reversible: true, rollbackAction: "restore previous .env", execPreview: "envsubst < /opt/worker/config.template > /opt/worker/.env" },
|
|
361
|
+
{ description: "Start worker and verify queue depth drops", action: "systemctl start", target: "synth-worker", reversible: true, rollbackAction: "systemctl stop synth-worker", execPreview: "systemctl start synth-worker" },
|
|
362
|
+
{ description: "Verify queue processing resumes within 30s", action: "verify health", target: "http://localhost:9090/metrics", reversible: false, execPreview: "curl -f --retry 6 --retry-delay 5 http://localhost:9090/metrics" },
|
|
363
|
+
],
|
|
364
|
+
reasoning: "Worker upgrade with concurrency increase. Drain first to avoid job loss, then replace binary and config atomically. Queue depth check confirms processing resumed.",
|
|
365
|
+
},
|
|
366
|
+
debriefEntryIds: [],
|
|
367
|
+
createdAt: hoursAgo(0.1),
|
|
368
|
+
};
|
|
369
|
+
const dep10 = {
|
|
370
|
+
id: crypto.randomUUID(), artifactId: webAppArtifact.id, partitionId: initechPartition.id,
|
|
371
|
+
environmentId: prodEnv.id, version: "2.4.1", status: "rolled_back",
|
|
372
|
+
variables: { ...initechPartition.variables, ...prodEnv.variables },
|
|
373
|
+
debriefEntryIds: [],
|
|
374
|
+
createdAt: hoursAgo(8), completedAt: hoursAgo(7.5),
|
|
375
|
+
failureReason: "Rolled back after post-deploy smoke test detected 502 errors on /api/v2/users",
|
|
376
|
+
};
|
|
377
|
+
for (const d of [dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8, dep9, dep10, dep11]) {
|
|
378
|
+
deployments.save(d);
|
|
379
|
+
}
|
|
380
|
+
// --- Security boundaries for envoys ---
|
|
381
|
+
const envoyId = "envoy-prod-1";
|
|
382
|
+
securityBoundaryStore.set(envoyId, [
|
|
383
|
+
{ id: crypto.randomUUID(), envoyId, boundaryType: "filesystem", config: { allowedPaths: ["/opt/synth", "/var/log/synth"], readOnly: ["/etc"], denied: ["/root", "/home"] } },
|
|
384
|
+
{ id: crypto.randomUUID(), envoyId, boundaryType: "network", config: { allowedHosts: ["db.internal", "redis.internal", "registry.internal"], allowedPorts: [5432, 6379, 443], deniedCidrs: ["10.0.0.0/8"] } },
|
|
385
|
+
{ id: crypto.randomUUID(), envoyId, boundaryType: "execution", config: { allowedCommands: ["docker", "npm", "systemctl", "curl"], deniedCommands: ["rm -rf", "dd", "mkfs"], maxTimeoutMs: 300000 } },
|
|
386
|
+
{ id: crypto.randomUUID(), envoyId, boundaryType: "credential", config: { allowedSecretPaths: ["synth/*"], deniedSecretPaths: ["admin/*", "root/*"], rotationRequired: true } },
|
|
387
|
+
]);
|
|
388
|
+
const stagingEnvoyId = "envoy-staging-1";
|
|
389
|
+
securityBoundaryStore.set(stagingEnvoyId, [
|
|
390
|
+
{ id: crypto.randomUUID(), envoyId: stagingEnvoyId, boundaryType: "filesystem", config: { allowedPaths: ["/opt/synth", "/var/log", "/tmp"], readOnly: ["/etc"] } },
|
|
391
|
+
{ id: crypto.randomUUID(), envoyId: stagingEnvoyId, boundaryType: "network", config: { allowedHosts: ["*"], allowedPorts: [5432, 6379, 443, 8080], deniedCidrs: [] } },
|
|
392
|
+
{ id: crypto.randomUUID(), envoyId: stagingEnvoyId, boundaryType: "execution", config: { allowedCommands: ["docker", "npm", "systemctl", "curl", "node"], deniedCommands: ["rm -rf"], maxTimeoutMs: 600000 } },
|
|
393
|
+
]);
|
|
394
|
+
// --- Debrief entries (rich decision diary) ---
|
|
395
|
+
debrief.record({
|
|
396
|
+
partitionId: null, deploymentId: null, agent: "server", decisionType: "system",
|
|
397
|
+
decision: "Command initialized with demo data",
|
|
398
|
+
reasoning: "Seeded 3 partitions, 3 environments, 3 artifacts, 10 deployments, and 2 envoy security boundary sets.",
|
|
399
|
+
context: { partitions: 3, environments: 3, deployments: 10, artifacts: 3, securityBoundaries: 2 },
|
|
400
|
+
});
|
|
401
|
+
// dep1 — web-app 2.3.0 succeeded
|
|
402
|
+
debrief.record({
|
|
403
|
+
partitionId: acmePartition.id, deploymentId: dep1.id, agent: "server", decisionType: "pipeline-plan",
|
|
404
|
+
decision: "Planned deployment pipeline for web-app v2.3.0 to Acme Corp production",
|
|
405
|
+
reasoning: "Standard 3-step pipeline: install deps, run migrations, health check. No variable conflicts.",
|
|
406
|
+
context: { version: "2.3.0", steps: 3 },
|
|
407
|
+
});
|
|
408
|
+
debrief.record({
|
|
409
|
+
partitionId: acmePartition.id, deploymentId: dep1.id, agent: "server", decisionType: "configuration-resolved",
|
|
410
|
+
decision: "Resolved 4 variables for Acme Corp production (partition + environment merged)",
|
|
411
|
+
reasoning: "Merged partition variables (APP_ENV, DB_HOST, REGION) with environment variables (APP_ENV, LOG_LEVEL). APP_ENV conflict resolved: environment value takes precedence.",
|
|
412
|
+
context: { resolvedCount: 4, conflicts: 1, policy: "environment-wins" },
|
|
413
|
+
});
|
|
414
|
+
debrief.record({
|
|
415
|
+
partitionId: acmePartition.id, deploymentId: dep1.id, agent: "envoy", decisionType: "deployment-execution",
|
|
416
|
+
decision: "Executed deployment web-app v2.3.0 on Acme Corp production",
|
|
417
|
+
reasoning: "All 3 steps completed. Total execution time: 28.4s.",
|
|
418
|
+
context: { duration: 28400 },
|
|
419
|
+
});
|
|
420
|
+
debrief.record({
|
|
421
|
+
partitionId: acmePartition.id, deploymentId: dep1.id, agent: "envoy", decisionType: "health-check",
|
|
422
|
+
decision: "Health check passed on first attempt",
|
|
423
|
+
reasoning: "GET /health returned 200 with body {\"status\":\"ok\"} in 45ms.",
|
|
424
|
+
context: { attempts: 1, responseTime: 45 },
|
|
425
|
+
});
|
|
426
|
+
debrief.record({
|
|
427
|
+
partitionId: acmePartition.id, deploymentId: dep1.id, agent: "server", decisionType: "deployment-completion",
|
|
428
|
+
decision: "Deployment web-app v2.3.0 completed successfully",
|
|
429
|
+
reasoning: "All pipeline steps passed. Health check confirmed. Marked as succeeded.",
|
|
430
|
+
context: { status: "succeeded" },
|
|
431
|
+
});
|
|
432
|
+
// dep4 — api-service 1.11.0 failed
|
|
433
|
+
debrief.record({
|
|
434
|
+
partitionId: acmePartition.id, deploymentId: dep4.id, agent: "server", decisionType: "pipeline-plan",
|
|
435
|
+
decision: "Planned deployment pipeline for api-service v1.11.0 to Acme Corp production",
|
|
436
|
+
reasoning: "2-step pipeline: pull image, verify endpoint.",
|
|
437
|
+
context: { version: "1.11.0", steps: 2 },
|
|
438
|
+
});
|
|
439
|
+
debrief.record({
|
|
440
|
+
partitionId: acmePartition.id, deploymentId: dep4.id, agent: "envoy", decisionType: "deployment-execution",
|
|
441
|
+
decision: "Image pull succeeded, starting verification",
|
|
442
|
+
reasoning: "docker pull completed in 12.3s. Image sha256:a4f8e... verified.",
|
|
443
|
+
context: { step: "Pull image", duration: 12300 },
|
|
444
|
+
});
|
|
445
|
+
debrief.record({
|
|
446
|
+
partitionId: acmePartition.id, deploymentId: dep4.id, agent: "envoy", decisionType: "health-check",
|
|
447
|
+
decision: "Health check failed after 3 retries",
|
|
448
|
+
reasoning: "Connection refused on port 8080. Retry 1: refused (5s). Retry 2: refused (10s). Retry 3: refused (15s). Container logs: \"Error: EADDRINUSE :::8080\".",
|
|
449
|
+
context: { attempts: 3, lastError: "ECONNREFUSED", containerLog: "EADDRINUSE" },
|
|
450
|
+
});
|
|
451
|
+
debrief.record({
|
|
452
|
+
partitionId: acmePartition.id, deploymentId: dep4.id, agent: "envoy", decisionType: "diagnostic-investigation",
|
|
453
|
+
decision: "Root cause: port 8080 bound by stale process from previous deployment",
|
|
454
|
+
reasoning: "Found zombie process from api-service v1.10.0 holding port 8080. Previous deployment did not cleanly shut down.",
|
|
455
|
+
context: { rootCause: "port-conflict", stalePid: 14823 },
|
|
456
|
+
});
|
|
457
|
+
debrief.record({
|
|
458
|
+
partitionId: acmePartition.id, deploymentId: dep4.id, agent: "server", decisionType: "deployment-failure",
|
|
459
|
+
decision: "Deployment api-service v1.11.0 failed — health check could not connect",
|
|
460
|
+
reasoning: "Envoy diagnostic identified port conflict from stale process. Recommend adding a pre-deploy cleanup step.",
|
|
461
|
+
context: { status: "failed", recommendation: "Add cleanup step" },
|
|
462
|
+
});
|
|
463
|
+
// dep7 — worker-service 2.9.0 failed
|
|
464
|
+
debrief.record({
|
|
465
|
+
partitionId: initechPartition.id, deploymentId: dep7.id, agent: "server", decisionType: "pipeline-plan",
|
|
466
|
+
decision: "Planned deployment pipeline for worker-service v2.9.0 to Initech production",
|
|
467
|
+
reasoning: "4-step pipeline with full verification strategy.",
|
|
468
|
+
context: { version: "2.9.0", steps: 4, verificationStrategy: "full" },
|
|
469
|
+
});
|
|
470
|
+
debrief.record({
|
|
471
|
+
partitionId: initechPartition.id, deploymentId: dep7.id, agent: "envoy", decisionType: "deployment-execution",
|
|
472
|
+
decision: "Workers stopped and binary deployed successfully",
|
|
473
|
+
reasoning: "Pre-deploy steps completed. Workers stopped gracefully (0 in-flight jobs lost). Binary copied.",
|
|
474
|
+
context: { stepsCompleted: 2, jobsLost: 0 },
|
|
475
|
+
});
|
|
476
|
+
debrief.record({
|
|
477
|
+
partitionId: initechPartition.id, deploymentId: dep7.id, agent: "envoy", decisionType: "deployment-verification",
|
|
478
|
+
decision: "Verification failed: queue depth 342 exceeds threshold of 100",
|
|
479
|
+
reasoning: "Workers restarted but queue depth grew rapidly. v2.9.0 introduced a regression in the message processing loop causing 10x slowdown.",
|
|
480
|
+
context: { queueDepth: 342, threshold: 100, processingRate: "0.3/s vs expected 3/s" },
|
|
481
|
+
});
|
|
482
|
+
debrief.record({
|
|
483
|
+
partitionId: initechPartition.id, deploymentId: dep7.id, agent: "server", decisionType: "deployment-failure",
|
|
484
|
+
decision: "Deployment worker-service v2.9.0 failed — queue depth exceeded threshold",
|
|
485
|
+
reasoning: "Queue depth check returned 342 (max 100). Processing regression in v2.9.0.",
|
|
486
|
+
context: { status: "failed" },
|
|
487
|
+
});
|
|
488
|
+
// dep10 — web-app 2.4.1 rolled back
|
|
489
|
+
debrief.record({
|
|
490
|
+
partitionId: initechPartition.id, deploymentId: dep10.id, agent: "server", decisionType: "pipeline-plan",
|
|
491
|
+
decision: "Planned deployment pipeline for web-app v2.4.1 to Initech production",
|
|
492
|
+
reasoning: "Standard 3-step pipeline.",
|
|
493
|
+
context: { version: "2.4.1", steps: 3 },
|
|
494
|
+
});
|
|
495
|
+
debrief.record({
|
|
496
|
+
partitionId: initechPartition.id, deploymentId: dep10.id, agent: "envoy", decisionType: "deployment-execution",
|
|
497
|
+
decision: "All deployment steps completed, starting post-deploy verification",
|
|
498
|
+
reasoning: "Dependencies installed (14.2s), migrations ran (3.1s), health check passed (0.2s).",
|
|
499
|
+
context: { totalDuration: 17500 },
|
|
500
|
+
});
|
|
501
|
+
debrief.record({
|
|
502
|
+
partitionId: initechPartition.id, deploymentId: dep10.id, agent: "envoy", decisionType: "deployment-verification",
|
|
503
|
+
decision: "Post-deploy smoke test detected 502 errors on /api/v2/users",
|
|
504
|
+
reasoning: "12 endpoint checks: 10 passed, 2 returned 502 (GET and POST /api/v2/users). The v2 users endpoint depends on a schema migration that was partially applied.",
|
|
505
|
+
context: { passed: 10, failed: 2, failedEndpoints: ["/api/v2/users"] },
|
|
506
|
+
});
|
|
507
|
+
debrief.record({
|
|
508
|
+
partitionId: initechPartition.id, deploymentId: dep10.id, agent: "server", decisionType: "deployment-failure",
|
|
509
|
+
decision: "Initiated rollback of web-app v2.4.1 on Initech production",
|
|
510
|
+
reasoning: "502 errors on critical user endpoints. Rolling back to previous known-good version.",
|
|
511
|
+
context: { status: "rolled_back", rolledBackFrom: "2.4.1" },
|
|
512
|
+
});
|
|
513
|
+
// dep6 — web-app 2.5.0-rc.1 with variable conflict
|
|
514
|
+
debrief.record({
|
|
515
|
+
partitionId: globexPartition.id, deploymentId: dep6.id, agent: "server", decisionType: "pipeline-plan",
|
|
516
|
+
decision: "Planned deployment for web-app v2.5.0-rc.1 to Globex staging",
|
|
517
|
+
reasoning: "Standard 3-step pipeline. Release candidate — permissive conflict policy.",
|
|
518
|
+
context: { version: "2.5.0-rc.1", steps: 3 },
|
|
519
|
+
});
|
|
520
|
+
debrief.record({
|
|
521
|
+
partitionId: globexPartition.id, deploymentId: dep6.id, agent: "server", decisionType: "variable-conflict",
|
|
522
|
+
decision: "Variable conflict: APP_ENV defined in both partition and environment",
|
|
523
|
+
reasoning: "Partition sets APP_ENV=production, environment sets APP_ENV=staging. Permissive policy — using environment value.",
|
|
524
|
+
context: { variable: "APP_ENV", partitionValue: "production", environmentValue: "staging", resolution: "environment-wins" },
|
|
525
|
+
});
|
|
526
|
+
debrief.record({
|
|
527
|
+
partitionId: globexPartition.id, deploymentId: dep6.id, agent: "server", decisionType: "deployment-completion",
|
|
528
|
+
decision: "Deployment web-app v2.5.0-rc.1 completed on Globex staging",
|
|
529
|
+
reasoning: "All steps passed despite variable conflict. RC verified in staging.",
|
|
530
|
+
context: { status: "succeeded" },
|
|
531
|
+
});
|
|
532
|
+
// dep9 — in-progress
|
|
533
|
+
debrief.record({
|
|
534
|
+
partitionId: globexPartition.id, deploymentId: dep9.id, agent: "server", decisionType: "pipeline-plan",
|
|
535
|
+
decision: "Planned deployment for api-service v1.13.0-beta.2 to Globex staging",
|
|
536
|
+
reasoning: "2-step pipeline for staging. Beta version — monitoring closely.",
|
|
537
|
+
context: { version: "1.13.0-beta.2", steps: 2 },
|
|
538
|
+
});
|
|
539
|
+
debrief.record({
|
|
540
|
+
partitionId: globexPartition.id, deploymentId: dep9.id, agent: "envoy", decisionType: "deployment-execution",
|
|
541
|
+
decision: "Image pull in progress for api-service v1.13.0-beta.2",
|
|
542
|
+
reasoning: "Pulling docker image from registry. Download progress: 67%.",
|
|
543
|
+
context: { step: "Pull image", progress: "67%" },
|
|
544
|
+
});
|
|
545
|
+
// Environment scans
|
|
546
|
+
debrief.record({
|
|
547
|
+
partitionId: acmePartition.id, deploymentId: null, agent: "envoy", decisionType: "environment-scan",
|
|
548
|
+
decision: "Environment scan completed for Acme Corp production",
|
|
549
|
+
reasoning: "Current versions: web-app v2.4.1, api-service v1.12.0. Disk: 62%. Memory: 71%. No drift detected.",
|
|
550
|
+
context: { versions: { "web-app": "2.4.1", "api-service": "1.12.0" }, diskUsage: "62%", memoryUsage: "71%" },
|
|
551
|
+
});
|
|
552
|
+
debrief.record({
|
|
553
|
+
partitionId: initechPartition.id, deploymentId: null, agent: "envoy", decisionType: "environment-scan",
|
|
554
|
+
decision: "Environment scan for Initech production — drift detected",
|
|
555
|
+
reasoning: "worker-service v3.0.0 running. web-app at v2.4.0 (v2.4.1 was rolled back). Drift: LOG_LEVEL manually changed from 'warn' to 'debug' outside deployment pipeline.",
|
|
556
|
+
context: { drift: true, driftDetails: "LOG_LEVEL changed outside pipeline" },
|
|
557
|
+
});
|
|
558
|
+
console.log('Demo seed data created (set SYNTH_SEED_DEMO=false to disable)');
|
|
559
|
+
}
|
|
560
|
+
else if (process.env.SYNTH_SEED_DEMO === 'false') {
|
|
561
|
+
console.log('Demo seed data skipped (SYNTH_SEED_DEMO=false)');
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
console.log('Demo seed data skipped (database already populated)');
|
|
565
|
+
}
|
|
566
|
+
// Seed demo envoys on every startup when demo mode is on — the registry is
|
|
567
|
+
// in-memory so it's always empty at boot, regardless of DB state.
|
|
568
|
+
if (process.env.SYNTH_SEED_DEMO !== 'false') {
|
|
569
|
+
const secsAgo = (s) => new Date(Date.now() - s * 1000).toISOString();
|
|
570
|
+
const e1 = envoyRegistry.register({ name: "stg-web-01", url: "http://stg-web-01.internal:8080", assignedEnvironments: ["staging"] });
|
|
571
|
+
e1.lastHealthStatus = "healthy";
|
|
572
|
+
e1.lastHealthCheck = secsAgo(12);
|
|
573
|
+
e1.cachedHostname = "stg-web-01.acme.internal";
|
|
574
|
+
e1.cachedOs = "Ubuntu 22.04";
|
|
575
|
+
e1.cachedSummary = { totalDeployments: 47, succeeded: 45, failed: 2, executing: 0, environments: 1 };
|
|
576
|
+
e1.cachedReadiness = { ready: true, reason: "Workspace is ready for deployments." };
|
|
577
|
+
const e2 = envoyRegistry.register({ name: "stg-web-02", url: "http://stg-web-02.internal:8080", assignedEnvironments: ["staging"] });
|
|
578
|
+
e2.lastHealthStatus = "healthy";
|
|
579
|
+
e2.lastHealthCheck = secsAgo(8);
|
|
580
|
+
e2.cachedHostname = "stg-web-02.acme.internal";
|
|
581
|
+
e2.cachedOs = "Ubuntu 22.04";
|
|
582
|
+
e2.cachedSummary = { totalDeployments: 44, succeeded: 43, failed: 1, executing: 0, environments: 1 };
|
|
583
|
+
e2.cachedReadiness = { ready: true, reason: "Workspace is ready for deployments." };
|
|
584
|
+
const e3 = envoyRegistry.register({ name: "prd-web-01", url: "http://prd-web-01.internal:8080", assignedEnvironments: ["production"] });
|
|
585
|
+
e3.lastHealthStatus = "healthy";
|
|
586
|
+
e3.lastHealthCheck = secsAgo(3);
|
|
587
|
+
e3.cachedHostname = "prd-web-01.acme.internal";
|
|
588
|
+
e3.cachedOs = "Ubuntu 22.04";
|
|
589
|
+
e3.cachedSummary = { totalDeployments: 112, succeeded: 110, failed: 2, executing: 0, environments: 1 };
|
|
590
|
+
e3.cachedReadiness = { ready: true, reason: "Workspace is ready for deployments." };
|
|
591
|
+
const e4 = envoyRegistry.register({ name: "prd-web-02", url: "http://prd-web-02.internal:8080", assignedEnvironments: ["production"] });
|
|
592
|
+
e4.lastHealthStatus = "degraded";
|
|
593
|
+
e4.lastHealthCheck = secsAgo(45);
|
|
594
|
+
e4.cachedHostname = "prd-web-02.acme.internal";
|
|
595
|
+
e4.cachedOs = "Ubuntu 20.04";
|
|
596
|
+
e4.cachedSummary = { totalDeployments: 108, succeeded: 104, failed: 4, executing: 0, environments: 1 };
|
|
597
|
+
e4.cachedReadiness = { ready: false, reason: "Heartbeat degraded — 45s since last check-in." };
|
|
598
|
+
const e5 = envoyRegistry.register({ name: "prd-batch-01", url: "http://prd-batch-01.internal:8080", assignedEnvironments: ["production"] });
|
|
599
|
+
e5.lastHealthStatus = "healthy";
|
|
600
|
+
e5.lastHealthCheck = secsAgo(6);
|
|
601
|
+
e5.cachedHostname = "prd-batch-01.acme.internal";
|
|
602
|
+
e5.cachedOs = "Windows Server 2022";
|
|
603
|
+
e5.cachedSummary = { totalDeployments: 23, succeeded: 23, failed: 0, executing: 0, environments: 1 };
|
|
604
|
+
e5.cachedReadiness = { ready: true, reason: "Workspace is ready for deployments." };
|
|
605
|
+
}
|
|
606
|
+
// --- Create MCP server ---
|
|
607
|
+
const mcp = createMcpServer({ agent, debrief, partitions, environments, deployments, artifactStore });
|
|
608
|
+
// --- Create Fastify HTTP server ---
|
|
609
|
+
const app = Fastify({ logger: true });
|
|
610
|
+
// Map EditionError → 402 Payment Required
|
|
611
|
+
app.setErrorHandler((error, _request, reply) => {
|
|
612
|
+
if (error instanceof EditionError) {
|
|
613
|
+
return reply.status(402).send({
|
|
614
|
+
error: "Enterprise feature",
|
|
615
|
+
feature: error.featureName,
|
|
616
|
+
message: error.message,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
// Let Fastify handle everything else
|
|
620
|
+
reply.send(error);
|
|
621
|
+
});
|
|
622
|
+
// Configure CORS origin from SYNTH_CORS_ORIGIN env var.
|
|
623
|
+
// If unset: reject all cross-origin (secure default). Single value: string. Comma-separated: string[].
|
|
624
|
+
const rawCorsOrigin = process.env.SYNTH_CORS_ORIGIN;
|
|
625
|
+
let corsOrigin;
|
|
626
|
+
if (!rawCorsOrigin || rawCorsOrigin.trim() === '') {
|
|
627
|
+
corsOrigin = false;
|
|
628
|
+
console.warn('[Synth] SYNTH_CORS_ORIGIN is not set — CORS will reject all cross-origin requests. Set it to your UI origin (e.g., http://localhost:5173) for development.');
|
|
629
|
+
}
|
|
630
|
+
else if (rawCorsOrigin.includes(',')) {
|
|
631
|
+
corsOrigin = rawCorsOrigin.split(',').map((o) => o.trim());
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
corsOrigin = rawCorsOrigin.trim();
|
|
635
|
+
}
|
|
636
|
+
await app.register(fastifyCors, {
|
|
637
|
+
origin: corsOrigin,
|
|
638
|
+
});
|
|
639
|
+
// Form body parsing (required for SAML POST callbacks)
|
|
640
|
+
await app.register(fastifyFormBody);
|
|
641
|
+
// Multipart/form-data parsing (required for file upload intake)
|
|
642
|
+
await app.register(fastifyMultipart, { limits: { fileSize: 100 * 1024 * 1024 } });
|
|
643
|
+
// Rate limiting — configurable via environment variables
|
|
644
|
+
await app.register(rateLimit, {
|
|
645
|
+
max: Number(process.env.SYNTH_RATE_LIMIT_MAX ?? 100),
|
|
646
|
+
timeWindow: Number(process.env.SYNTH_RATE_LIMIT_WINDOW_MS ?? 60_000),
|
|
647
|
+
});
|
|
648
|
+
// Register authentication middleware
|
|
649
|
+
const auth = registerAuthMiddleware(app, userStore, userRoleStore, sessionStore, jwtSecret);
|
|
650
|
+
// Register REST routes
|
|
651
|
+
registerHealthRoutes(app, {
|
|
652
|
+
entityDb,
|
|
653
|
+
dataDir: DATA_DIR,
|
|
654
|
+
envoyUrl,
|
|
655
|
+
llmApiKey: process.env.SYNTH_LLM_API_KEY,
|
|
656
|
+
llmBaseUrl: process.env.SYNTH_LLM_BASE_URL,
|
|
657
|
+
mcpServers: settings.get().mcpServers,
|
|
658
|
+
llmClient: llm,
|
|
659
|
+
});
|
|
660
|
+
const progressStore = new ProgressEventStore();
|
|
661
|
+
const defaultEnvoyClient = new EnvoyClient(settings.get().envoy.url, settings.get().envoy.timeoutMs);
|
|
662
|
+
registerDeploymentRoutes(app, deployments, debrief, partitions, environments, artifactStore, settings, telemetryStore, progressStore, defaultEnvoyClient, envoyRegistry, llm);
|
|
663
|
+
registerEnvoyReportRoutes(app, debrief, deployments, envoyRegistry);
|
|
664
|
+
registerArtifactRoutes(app, artifactStore, telemetryStore, artifactAnalyzer);
|
|
665
|
+
registerSecurityBoundaryRoutes(app, securityBoundaryStore, telemetryStore);
|
|
666
|
+
registerPartitionRoutes(app, partitions, deployments, debrief, telemetryStore);
|
|
667
|
+
registerEnvironmentRoutes(app, environments, deployments, telemetryStore);
|
|
668
|
+
registerAgentRoutes(app, agent, partitions, environments, artifactStore, deployments, debrief, settings, llm, envoyRegistry, telemetryStore, artifactAnalyzer);
|
|
669
|
+
registerSettingsRoutes(app, settings, telemetryStore);
|
|
670
|
+
registerTelemetryRoutes(app, telemetryStore);
|
|
671
|
+
registerEnvoyRoutes(app, settings, envoyRegistry, telemetryStore, deployments, debrief);
|
|
672
|
+
registerSystemRoutes(app, deployments, artifactStore, environments, partitions, envoyRegistry);
|
|
673
|
+
registerAuthRoutes(app, userStore, roleStore, userRoleStore, sessionStore, jwtSecret);
|
|
674
|
+
registerApiKeyRoutes(app, apiKeyStore, jwtSecret);
|
|
675
|
+
registerUserRoutes(app, userStore, roleStore, userRoleStore);
|
|
676
|
+
registerIdpRoutes(app, idpProviderStore, roleMappingStore, userStore, roleStore, userRoleStore, sessionStore, jwtSecret, { hasDedicatedEncryptionKey });
|
|
677
|
+
// Fleet (large-scale) deployment orchestration
|
|
678
|
+
const fleetStore = new FleetDeploymentStore(entityDb);
|
|
679
|
+
const fleetExecutor = new FleetExecutor(envoyRegistry, (url, token) => new EnvoyClient(url));
|
|
680
|
+
registerFleetRoutes(app, fleetStore, envoyRegistry, deployments, fleetExecutor, debrief);
|
|
681
|
+
// --- Deployment Graph Orchestration ---
|
|
682
|
+
const graphStore = new DeploymentGraphStore();
|
|
683
|
+
const graphInferenceEngine = new GraphInferenceEngine(llm, artifactStore);
|
|
684
|
+
registerGraphRoutes(app, graphStore, graphInferenceEngine, envoyRegistry, artifactStore, debrief);
|
|
685
|
+
// --- Artifact Intake Pipeline ---
|
|
686
|
+
const intakeChannelStore = new IntakeChannelStore(entityDb);
|
|
687
|
+
const intakeEventStore = new IntakeEventStore(entityDb);
|
|
688
|
+
const intakeProcessor = new IntakeProcessor(artifactStore, artifactAnalyzer);
|
|
689
|
+
const registryPollerVersionStore = new PersistentRegistryPollerVersionStore(entityDb);
|
|
690
|
+
const registryPoller = new RegistryPoller(async (channelId, payload) => {
|
|
691
|
+
const event = intakeEventStore.create({ channelId, status: "processing", payload: payload });
|
|
692
|
+
try {
|
|
693
|
+
const result = await intakeProcessor.process(payload, channelId);
|
|
694
|
+
intakeEventStore.update(event.id, { status: "completed", artifactId: result.artifactId, processedAt: new Date() });
|
|
695
|
+
}
|
|
696
|
+
catch (err) {
|
|
697
|
+
intakeEventStore.update(event.id, { status: "failed", error: err instanceof Error ? err.message : "Processing failed", processedAt: new Date() });
|
|
698
|
+
}
|
|
699
|
+
}, registryPollerVersionStore);
|
|
700
|
+
registerIntakeRoutes(app, intakeChannelStore, intakeEventStore, intakeProcessor, registryPoller, artifactStore);
|
|
701
|
+
// Start polling for any pre-existing enabled registry channels
|
|
702
|
+
for (const ch of intakeChannelStore.list()) {
|
|
703
|
+
if (ch.type === "registry" && ch.enabled) {
|
|
704
|
+
registryPoller.startPolling(ch);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
// --- Serve UI static files if built ---
|
|
708
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
709
|
+
const uiDistPath = path.resolve(__dirname, "../../ui/dist");
|
|
710
|
+
if (existsSync(uiDistPath)) {
|
|
711
|
+
await app.register(fastifyStatic, {
|
|
712
|
+
root: uiDistPath,
|
|
713
|
+
prefix: "/",
|
|
714
|
+
wildcard: false,
|
|
715
|
+
});
|
|
716
|
+
// SPA fallback: serve index.html for unmatched routes
|
|
717
|
+
app.setNotFoundHandler(async (_request, reply) => {
|
|
718
|
+
return reply.sendFile("index.html", uiDistPath);
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
const mcpTransports = new Map();
|
|
722
|
+
const MCP_SESSION_TTL_MS = Number(process.env.SYNTH_MCP_SESSION_TTL_MS ?? 60 * 60 * 1000);
|
|
723
|
+
app.post("/mcp", async (request, reply) => {
|
|
724
|
+
const sessionId = request.headers["mcp-session-id"] ?? undefined;
|
|
725
|
+
let transport;
|
|
726
|
+
if (sessionId && mcpTransports.has(sessionId)) {
|
|
727
|
+
transport = mcpTransports.get(sessionId).transport;
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
transport = new StreamableHTTPServerTransport({
|
|
731
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
732
|
+
});
|
|
733
|
+
await mcp.connect(transport);
|
|
734
|
+
// After connect, the transport has a sessionId
|
|
735
|
+
const newSessionId = transport.sessionId;
|
|
736
|
+
if (newSessionId) {
|
|
737
|
+
mcpTransports.set(newSessionId, { transport, createdAt: Date.now() });
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
// Hand off to the MCP transport, passing raw Node.js req/res
|
|
741
|
+
await transport.handleRequest(request.raw, reply.raw, request.body);
|
|
742
|
+
reply.hijack();
|
|
743
|
+
});
|
|
744
|
+
// Handle GET for SSE stream (server-to-client notifications)
|
|
745
|
+
app.get("/mcp", async (request, reply) => {
|
|
746
|
+
const sessionId = request.headers["mcp-session-id"];
|
|
747
|
+
if (!sessionId || !mcpTransports.has(sessionId)) {
|
|
748
|
+
return reply.status(400).send({ error: "Invalid or missing session ID" });
|
|
749
|
+
}
|
|
750
|
+
const transport = mcpTransports.get(sessionId).transport;
|
|
751
|
+
await transport.handleRequest(request.raw, reply.raw);
|
|
752
|
+
reply.hijack();
|
|
753
|
+
});
|
|
754
|
+
// Handle DELETE for session cleanup
|
|
755
|
+
app.delete("/mcp", async (request, reply) => {
|
|
756
|
+
const sessionId = request.headers["mcp-session-id"];
|
|
757
|
+
if (sessionId && mcpTransports.has(sessionId)) {
|
|
758
|
+
const session = mcpTransports.get(sessionId);
|
|
759
|
+
await session.transport.close();
|
|
760
|
+
mcpTransports.delete(sessionId);
|
|
761
|
+
}
|
|
762
|
+
return reply.status(200).send({ status: "session closed" });
|
|
763
|
+
});
|
|
764
|
+
// Periodic MCP session cleanup (every 10 minutes)
|
|
765
|
+
const MCP_CLEANUP_INTERVAL_MS = Number(process.env.SYNTH_MCP_CLEANUP_INTERVAL_MS ?? 10 * 60 * 1000);
|
|
766
|
+
const mcpCleanupInterval = setInterval(async () => {
|
|
767
|
+
const now = Date.now();
|
|
768
|
+
let closed = 0;
|
|
769
|
+
for (const [id, session] of mcpTransports) {
|
|
770
|
+
if (now - session.createdAt > MCP_SESSION_TTL_MS) {
|
|
771
|
+
try {
|
|
772
|
+
await session.transport.close();
|
|
773
|
+
}
|
|
774
|
+
catch { /* already closed */ }
|
|
775
|
+
mcpTransports.delete(id);
|
|
776
|
+
closed++;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
if (closed > 0) {
|
|
780
|
+
app.log.info(`Cleaned up ${closed} expired MCP session(s)`);
|
|
781
|
+
}
|
|
782
|
+
}, MCP_CLEANUP_INTERVAL_MS);
|
|
783
|
+
// --- Start stale deployment scanner ---
|
|
784
|
+
const stopStaleScanner = startStaleDeploymentScanner(deployments, debrief);
|
|
785
|
+
const stopRetentionScanner = startRetentionScanner(debrief);
|
|
786
|
+
// --- Graceful shutdown hook ---
|
|
787
|
+
app.addHook("onClose", async () => {
|
|
788
|
+
stopStaleScanner();
|
|
789
|
+
stopRetentionScanner();
|
|
790
|
+
registryPoller.stopAll();
|
|
791
|
+
clearInterval(mcpCleanupInterval);
|
|
792
|
+
await mcpClientManager.disconnectAll();
|
|
793
|
+
debrief.close();
|
|
794
|
+
entityDb.close();
|
|
795
|
+
for (const session of mcpTransports.values()) {
|
|
796
|
+
await session.transport.close();
|
|
797
|
+
}
|
|
798
|
+
mcpTransports.clear();
|
|
799
|
+
console.log("Synth shutting down — resources cleaned up");
|
|
800
|
+
});
|
|
801
|
+
// --- Start ---
|
|
802
|
+
const PORT = parseInt(process.env.PORT ?? "9410", 10);
|
|
803
|
+
const HOST = process.env.HOST ?? "0.0.0.0";
|
|
804
|
+
app.listen({ port: PORT, host: HOST }, (err) => {
|
|
805
|
+
if (err) {
|
|
806
|
+
app.log.error(err);
|
|
807
|
+
process.exit(1);
|
|
808
|
+
}
|
|
809
|
+
const uiStatus = existsSync(uiDistPath) ? `UI: http://${HOST}:${PORT}/` : "UI: not built (run npm run build:ui)";
|
|
810
|
+
const authStatus = auth.enabled
|
|
811
|
+
? "Auth: enabled (JWT) "
|
|
812
|
+
: "Auth: disabled ";
|
|
813
|
+
const seedStatus = process.env.SYNTH_SEED_DEMO !== 'false'
|
|
814
|
+
? "Seed: 3 partitions, 3 envs, 3 artifacts \n║ 10 deployments, 2 boundaries "
|
|
815
|
+
: "Seed: disabled (SYNTH_SEED_DEMO=false) ";
|
|
816
|
+
console.log(`
|
|
817
|
+
╔══════════════════════════════════════════════════════╗
|
|
818
|
+
║ Synth v0.1.0 ║
|
|
819
|
+
║ ║
|
|
820
|
+
║ REST API: http://${HOST}:${PORT}/api ║
|
|
821
|
+
║ MCP: http://${HOST}:${PORT}/mcp ║
|
|
822
|
+
║ Health: http://${HOST}:${PORT}/health ║
|
|
823
|
+
║ ${uiStatus.padEnd(50)}║
|
|
824
|
+
║ ${authStatus}║
|
|
825
|
+
║ ║
|
|
826
|
+
║ ${seedStatus}║
|
|
827
|
+
╚══════════════════════════════════════════════════════╝
|
|
828
|
+
`);
|
|
829
|
+
const shutdown = async (signal) => {
|
|
830
|
+
console.log(`\nReceived ${signal}, shutting down gracefully...`);
|
|
831
|
+
await app.close();
|
|
832
|
+
process.exit(0);
|
|
833
|
+
};
|
|
834
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
835
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
836
|
+
});
|
|
837
|
+
//# sourceMappingURL=index.js.map
|