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