@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
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
import type { IEnvironmentStore, IArtifactStore, ITelemetryStore } from "@synth-deploy/core";
|
|
3
|
+
import { CreateEnvironmentSchema, UpdateEnvironmentSchema } from "./schemas.js";
|
|
4
|
+
import { requirePermission } from "../middleware/permissions.js";
|
|
5
|
+
import type { DeploymentStore } from "../agent/synth-agent.js";
|
|
6
|
+
|
|
7
|
+
export function registerEnvironmentRoutes(
|
|
8
|
+
app: FastifyInstance,
|
|
9
|
+
environments: IEnvironmentStore,
|
|
10
|
+
deployments: DeploymentStore,
|
|
11
|
+
telemetry: ITelemetryStore,
|
|
12
|
+
): void {
|
|
13
|
+
// List all environments
|
|
14
|
+
app.get("/api/environments", { preHandler: [requirePermission("environment.view")] }, async () => {
|
|
15
|
+
return { environments: environments.list() };
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Create an environment
|
|
19
|
+
app.post("/api/environments", { preHandler: [requirePermission("environment.create")] }, async (request, reply) => {
|
|
20
|
+
const parsed = CreateEnvironmentSchema.safeParse(request.body);
|
|
21
|
+
if (!parsed.success) {
|
|
22
|
+
return reply.status(400).send({ error: "Invalid input", details: parsed.error.format() });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const environment = environments.create(parsed.data.name.trim(), parsed.data.variables ?? {});
|
|
26
|
+
telemetry.record({ actor: (request.user?.email) ?? "anonymous", action: "environment.created", target: { type: "environment", id: environment.id }, details: { name: parsed.data.name } });
|
|
27
|
+
return reply.status(201).send({ environment });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Get environment by ID
|
|
31
|
+
app.get<{ Params: { id: string } }>(
|
|
32
|
+
"/api/environments/:id",
|
|
33
|
+
{ preHandler: [requirePermission("environment.view")] },
|
|
34
|
+
async (request, reply) => {
|
|
35
|
+
const environment = environments.get(request.params.id);
|
|
36
|
+
if (!environment) {
|
|
37
|
+
return reply.status(404).send({ error: "Environment not found" });
|
|
38
|
+
}
|
|
39
|
+
return { environment };
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Update environment
|
|
44
|
+
app.put<{ Params: { id: string } }>(
|
|
45
|
+
"/api/environments/:id",
|
|
46
|
+
{ preHandler: [requirePermission("environment.update")] },
|
|
47
|
+
async (request, reply) => {
|
|
48
|
+
const parsed = UpdateEnvironmentSchema.safeParse(request.body);
|
|
49
|
+
if (!parsed.success) {
|
|
50
|
+
return reply.status(400).send({ error: "Invalid input", details: parsed.error.format() });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const environment = environments.update(request.params.id, {
|
|
55
|
+
name: parsed.data.name?.trim(),
|
|
56
|
+
variables: parsed.data.variables,
|
|
57
|
+
});
|
|
58
|
+
telemetry.record({ actor: (request.user?.email) ?? "anonymous", action: "environment.updated", target: { type: "environment", id: request.params.id }, details: { name: parsed.data.name } });
|
|
59
|
+
return { environment };
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (err instanceof Error && err.message.toLowerCase().includes("not found")) {
|
|
62
|
+
return reply.status(404).send({ error: "Environment not found" });
|
|
63
|
+
}
|
|
64
|
+
app.log.error(err, "Failed to update environment");
|
|
65
|
+
return reply.status(500).send({ error: "Internal server error" });
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Delete environment (with linked-operations safety check)
|
|
71
|
+
app.delete<{ Params: { id: string } }>(
|
|
72
|
+
"/api/environments/:id",
|
|
73
|
+
{ preHandler: [requirePermission("environment.delete")] },
|
|
74
|
+
async (request, reply) => {
|
|
75
|
+
const envId = request.params.id;
|
|
76
|
+
const env = environments.get(envId);
|
|
77
|
+
if (!env) {
|
|
78
|
+
return reply.status(404).send({ error: "Environment not found" });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check if any deployments reference this environment
|
|
82
|
+
const linkedDeployments = deployments
|
|
83
|
+
.list()
|
|
84
|
+
.filter((d) => d.environmentId === envId);
|
|
85
|
+
|
|
86
|
+
if (linkedDeployments.length > 0) {
|
|
87
|
+
return reply.status(409).send({
|
|
88
|
+
error: `Environment has ${linkedDeployments.length} deployment(s). Cannot delete an environment with deployment history.`,
|
|
89
|
+
deploymentCount: linkedDeployments.length,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
environments.delete(envId);
|
|
94
|
+
return { deleted: true };
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { DecisionTypeEnum } from "@synth-deploy/core";
|
|
4
|
+
import type { DebriefWriter, DecisionType } from "@synth-deploy/core";
|
|
5
|
+
import type { DeploymentStore } from "../agent/synth-agent.js";
|
|
6
|
+
import type { EnvoyRegistry } from "../agent/envoy-registry.js";
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Schema — validates incoming Envoy reports
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
const DebriefEntrySchema = z.object({
|
|
13
|
+
id: z.string(),
|
|
14
|
+
timestamp: z.string(),
|
|
15
|
+
partitionId: z.string().nullable(),
|
|
16
|
+
deploymentId: z.string().nullable(),
|
|
17
|
+
agent: z.enum(["server", "envoy"]),
|
|
18
|
+
decisionType: DecisionTypeEnum,
|
|
19
|
+
decision: z.string(),
|
|
20
|
+
reasoning: z.string(),
|
|
21
|
+
context: z.record(z.unknown()),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const EnvoyReportSchema = z.object({
|
|
25
|
+
type: z.literal("deployment-result"),
|
|
26
|
+
envoyId: z.string(),
|
|
27
|
+
deploymentId: z.string(),
|
|
28
|
+
success: z.boolean(),
|
|
29
|
+
failureReason: z.string().nullable(),
|
|
30
|
+
debriefEntries: z.array(DebriefEntrySchema),
|
|
31
|
+
summary: z.object({
|
|
32
|
+
artifacts: z.array(z.string()),
|
|
33
|
+
workspacePath: z.string(),
|
|
34
|
+
executionDurationMs: z.number(),
|
|
35
|
+
totalDurationMs: z.number(),
|
|
36
|
+
verificationPassed: z.boolean(),
|
|
37
|
+
verificationChecks: z.array(
|
|
38
|
+
z.object({
|
|
39
|
+
name: z.string(),
|
|
40
|
+
passed: z.boolean(),
|
|
41
|
+
detail: z.string(),
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Route registration
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Endpoint for Envoys to push reports back to Command.
|
|
53
|
+
*
|
|
54
|
+
* When a Envoy completes a deployment (success or failure), it pushes
|
|
55
|
+
* a report containing its full debrief entries. Command ingests these
|
|
56
|
+
* into its own debrief so there is one unified Debrief that contains
|
|
57
|
+
* both Command's orchestration decisions and the Envoy's execution
|
|
58
|
+
* decisions.
|
|
59
|
+
*
|
|
60
|
+
* This is the Envoy->Command direction of bidirectional communication.
|
|
61
|
+
*/
|
|
62
|
+
export function registerEnvoyReportRoutes(
|
|
63
|
+
app: FastifyInstance,
|
|
64
|
+
debrief: DebriefWriter,
|
|
65
|
+
deployments: DeploymentStore,
|
|
66
|
+
registry: EnvoyRegistry,
|
|
67
|
+
): void {
|
|
68
|
+
app.post("/api/envoy/report", async (request, reply) => {
|
|
69
|
+
const authHeader = (request.headers.authorization ?? "") as string;
|
|
70
|
+
const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
71
|
+
if (!token || !registry.validateToken(token)) {
|
|
72
|
+
return reply.status(401).send({ error: "Invalid or missing envoy token" });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const parsed = EnvoyReportSchema.safeParse(request.body);
|
|
76
|
+
if (!parsed.success) {
|
|
77
|
+
return reply.status(400).send({
|
|
78
|
+
error: "Invalid Envoy report",
|
|
79
|
+
details: parsed.error.format(),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const report = parsed.data;
|
|
84
|
+
|
|
85
|
+
// Validate partition boundary: each debrief entry's deploymentId must
|
|
86
|
+
// belong to the claimed partitionId. Reject cross-partition reports.
|
|
87
|
+
for (const entry of report.debriefEntries) {
|
|
88
|
+
if (entry.deploymentId && entry.partitionId) {
|
|
89
|
+
const deployment = deployments.get(entry.deploymentId);
|
|
90
|
+
if (!deployment || deployment.partitionId !== entry.partitionId) {
|
|
91
|
+
debrief.record({
|
|
92
|
+
partitionId: entry.partitionId,
|
|
93
|
+
deploymentId: entry.deploymentId,
|
|
94
|
+
agent: "server",
|
|
95
|
+
decisionType: "system",
|
|
96
|
+
decision: "Rejected Envoy report: partition boundary violation",
|
|
97
|
+
reasoning: `Deployment ${entry.deploymentId} does not belong to partition ${entry.partitionId}. Report from envoy ${report.envoyId} rejected.`,
|
|
98
|
+
context: { envoyId: report.envoyId, reportedPartitionId: entry.partitionId },
|
|
99
|
+
});
|
|
100
|
+
return reply.status(403).send({
|
|
101
|
+
error: "Partition boundary violation",
|
|
102
|
+
detail: `Deployment ${entry.deploymentId} does not belong to partition ${entry.partitionId}`,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let ingested = 0;
|
|
109
|
+
|
|
110
|
+
// Ingest each Envoy debrief entry into Command's debrief.
|
|
111
|
+
// We re-record them (rather than inserting raw) so Command's debrief
|
|
112
|
+
// assigns its own IDs and timestamps. The original Envoy entry data
|
|
113
|
+
// is preserved in the context field for traceability.
|
|
114
|
+
for (const entry of report.debriefEntries) {
|
|
115
|
+
debrief.record({
|
|
116
|
+
partitionId: entry.partitionId,
|
|
117
|
+
deploymentId: entry.deploymentId,
|
|
118
|
+
agent: entry.agent as "server" | "envoy",
|
|
119
|
+
decisionType: entry.decisionType as DecisionType,
|
|
120
|
+
decision: entry.decision,
|
|
121
|
+
reasoning: entry.reasoning,
|
|
122
|
+
context: {
|
|
123
|
+
...entry.context,
|
|
124
|
+
_envoyReport: {
|
|
125
|
+
envoyId: report.envoyId,
|
|
126
|
+
originalEntryId: entry.id,
|
|
127
|
+
originalTimestamp: entry.timestamp,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
ingested++;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Update deployment status based on the result
|
|
135
|
+
const deployment = deployments.get(report.deploymentId);
|
|
136
|
+
if (deployment && deployment.status === "running") {
|
|
137
|
+
let finalStatus: typeof deployment.status;
|
|
138
|
+
if (report.success) {
|
|
139
|
+
finalStatus = "succeeded" as typeof deployment.status;
|
|
140
|
+
} else {
|
|
141
|
+
const hadRollback = report.debriefEntries.some((e) => e.decisionType === "rollback-execution");
|
|
142
|
+
finalStatus = (hadRollback ? "rolled_back" : "failed") as typeof deployment.status;
|
|
143
|
+
}
|
|
144
|
+
deployment.status = finalStatus;
|
|
145
|
+
if (!report.success && report.failureReason) {
|
|
146
|
+
deployment.failureReason = report.failureReason;
|
|
147
|
+
}
|
|
148
|
+
deployment.completedAt = new Date();
|
|
149
|
+
deployments.save(deployment);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return reply.status(200).send({
|
|
153
|
+
accepted: true,
|
|
154
|
+
deploymentId: report.deploymentId,
|
|
155
|
+
envoyId: report.envoyId,
|
|
156
|
+
entriesIngested: ingested,
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
import type { ISettingsStore, ITelemetryStore, IDeploymentStore, DebriefReader } from "@synth-deploy/core";
|
|
3
|
+
import type { EnvoyRegistry } from "../agent/envoy-registry.js";
|
|
4
|
+
import { requirePermission } from "../middleware/permissions.js";
|
|
5
|
+
import { getMaxEnvoys, EditionError } from "@synth-deploy/core";
|
|
6
|
+
|
|
7
|
+
export function registerEnvoyRoutes(
|
|
8
|
+
app: FastifyInstance,
|
|
9
|
+
settings: ISettingsStore,
|
|
10
|
+
registry: EnvoyRegistry,
|
|
11
|
+
telemetry: ITelemetryStore,
|
|
12
|
+
deployments: IDeploymentStore,
|
|
13
|
+
debrief: DebriefReader,
|
|
14
|
+
): void {
|
|
15
|
+
// List all registered Envoys (cached data — no live probe)
|
|
16
|
+
app.get("/api/envoys", { preHandler: [requirePermission("envoy.view")] }, async () => {
|
|
17
|
+
let entries = registry.listEntries();
|
|
18
|
+
|
|
19
|
+
// Also include the legacy settings-based default envoy if no registry entries
|
|
20
|
+
if (entries.length === 0) {
|
|
21
|
+
const envoyConfig = settings.get().envoy;
|
|
22
|
+
if (envoyConfig?.url) {
|
|
23
|
+
const legacy = registry.register({
|
|
24
|
+
name: "default",
|
|
25
|
+
url: envoyConfig.url,
|
|
26
|
+
});
|
|
27
|
+
const entry = registry.get(legacy.id);
|
|
28
|
+
if (entry) entries = [entry];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
envoys: entries.map((e) => ({
|
|
34
|
+
id: e.id,
|
|
35
|
+
name: e.name,
|
|
36
|
+
url: e.url,
|
|
37
|
+
health: e.health,
|
|
38
|
+
hostname: e.hostname,
|
|
39
|
+
os: e.os,
|
|
40
|
+
lastSeen: e.lastSeen,
|
|
41
|
+
summary: e.summary,
|
|
42
|
+
readiness: e.readiness,
|
|
43
|
+
assignedEnvironments: e.assignedEnvironments,
|
|
44
|
+
assignedPartitions: e.assignedPartitions,
|
|
45
|
+
})),
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Register a new Envoy
|
|
50
|
+
app.post("/api/envoys", { preHandler: [requirePermission("envoy.register")] }, async (request, reply) => {
|
|
51
|
+
const body = request.body as {
|
|
52
|
+
name: string;
|
|
53
|
+
url: string;
|
|
54
|
+
assignedEnvironments?: string[];
|
|
55
|
+
assignedPartitions?: string[];
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (!body.name || !body.url) {
|
|
59
|
+
return reply.status(400).send({ error: "name and url are required" });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Enforce envoy count limit (Community: 10, Enterprise: license value)
|
|
63
|
+
const maxEnvoys = getMaxEnvoys();
|
|
64
|
+
if (maxEnvoys > 0 && registry.listEntries().length >= maxEnvoys) {
|
|
65
|
+
throw new EditionError("unlimited-envoys");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const registration = registry.register({
|
|
69
|
+
name: body.name,
|
|
70
|
+
url: body.url,
|
|
71
|
+
assignedEnvironments: body.assignedEnvironments,
|
|
72
|
+
assignedPartitions: body.assignedPartitions,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Probe health immediately after registration
|
|
76
|
+
const entry = await registry.probe(registration.id);
|
|
77
|
+
|
|
78
|
+
telemetry.record({ actor: (request.user?.email) ?? "anonymous", action: "envoy.registered", target: { type: "envoy", id: registration.id }, details: { name: body.name, url: body.url } });
|
|
79
|
+
|
|
80
|
+
return reply.status(201).send({
|
|
81
|
+
envoy: {
|
|
82
|
+
id: registration.id,
|
|
83
|
+
name: registration.name,
|
|
84
|
+
url: registration.url,
|
|
85
|
+
token: registration.token,
|
|
86
|
+
assignedEnvironments: registration.assignedEnvironments,
|
|
87
|
+
assignedPartitions: registration.assignedPartitions,
|
|
88
|
+
registeredAt: registration.registeredAt,
|
|
89
|
+
health: entry?.health ?? "Unreachable",
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Get a specific Envoy's cached status (instant, no live probe)
|
|
95
|
+
app.get("/api/envoys/:id/health", { preHandler: [requirePermission("envoy.view")] }, async (request, reply) => {
|
|
96
|
+
const { id } = request.params as { id: string };
|
|
97
|
+
const entry = registry.get(id);
|
|
98
|
+
|
|
99
|
+
if (!entry) {
|
|
100
|
+
return reply.status(404).send({ error: "Envoy not found" });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
envoy: {
|
|
105
|
+
id: entry.id,
|
|
106
|
+
name: entry.name,
|
|
107
|
+
url: entry.url,
|
|
108
|
+
health: entry.health,
|
|
109
|
+
hostname: entry.hostname,
|
|
110
|
+
os: entry.os,
|
|
111
|
+
lastSeen: entry.lastSeen,
|
|
112
|
+
summary: entry.summary,
|
|
113
|
+
readiness: entry.readiness,
|
|
114
|
+
assignedEnvironments: entry.assignedEnvironments,
|
|
115
|
+
assignedPartitions: entry.assignedPartitions,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Update an Envoy's configuration
|
|
121
|
+
app.put("/api/envoys/:id", { preHandler: [requirePermission("envoy.configure")] }, async (request, reply) => {
|
|
122
|
+
const { id } = request.params as { id: string };
|
|
123
|
+
const body = request.body as {
|
|
124
|
+
name?: string;
|
|
125
|
+
url?: string;
|
|
126
|
+
assignedEnvironments?: string[];
|
|
127
|
+
assignedPartitions?: string[];
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const existing = registry.get(id);
|
|
131
|
+
const updated = registry.update(id, body);
|
|
132
|
+
if (!updated) {
|
|
133
|
+
return reply.status(404).send({ error: "Envoy not found" });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const actor = (request.user?.email) ?? "anonymous";
|
|
137
|
+
if (body.assignedEnvironments !== undefined && existing) {
|
|
138
|
+
const prev = new Set(existing.assignedEnvironments);
|
|
139
|
+
const next = new Set(body.assignedEnvironments);
|
|
140
|
+
for (const envId of next) {
|
|
141
|
+
if (!prev.has(envId)) telemetry.record({ actor, action: "envoy.connection.added", target: { type: "envoy", id }, details: { connectionType: "environment", targetId: envId } });
|
|
142
|
+
}
|
|
143
|
+
for (const envId of prev) {
|
|
144
|
+
if (!next.has(envId)) telemetry.record({ actor, action: "envoy.connection.removed", target: { type: "envoy", id }, details: { connectionType: "environment", targetId: envId } });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (body.assignedPartitions !== undefined && existing) {
|
|
148
|
+
const prev = new Set(existing.assignedPartitions);
|
|
149
|
+
const next = new Set(body.assignedPartitions);
|
|
150
|
+
for (const partId of next) {
|
|
151
|
+
if (!prev.has(partId)) telemetry.record({ actor, action: "envoy.connection.added", target: { type: "envoy", id }, details: { connectionType: "partition", targetId: partId } });
|
|
152
|
+
}
|
|
153
|
+
for (const partId of prev) {
|
|
154
|
+
if (!next.has(partId)) telemetry.record({ actor, action: "envoy.connection.removed", target: { type: "envoy", id }, details: { connectionType: "partition", targetId: partId } });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
envoy: {
|
|
160
|
+
id: updated.id,
|
|
161
|
+
name: updated.name,
|
|
162
|
+
url: updated.url,
|
|
163
|
+
assignedEnvironments: updated.assignedEnvironments,
|
|
164
|
+
assignedPartitions: updated.assignedPartitions,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Deregister an Envoy
|
|
170
|
+
app.delete("/api/envoys/:id", { preHandler: [requirePermission("envoy.configure")] }, async (request, reply) => {
|
|
171
|
+
const { id } = request.params as { id: string };
|
|
172
|
+
const removed = registry.deregister(id);
|
|
173
|
+
|
|
174
|
+
if (!removed) {
|
|
175
|
+
return reply.status(404).send({ error: "Envoy not found" });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return reply.status(204).send();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Rotate an Envoy's token
|
|
182
|
+
app.post("/api/envoys/:id/rotate-token", { preHandler: [requirePermission("envoy.configure")] }, async (request, reply) => {
|
|
183
|
+
const { id } = request.params as { id: string };
|
|
184
|
+
const newToken = registry.rotateToken(id);
|
|
185
|
+
|
|
186
|
+
if (!newToken) {
|
|
187
|
+
return reply.status(404).send({ error: "Envoy not found" });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { token: newToken };
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Get accumulated knowledge for an Envoy — system observations from environment scans
|
|
194
|
+
app.get("/api/envoys/:id/knowledge", { preHandler: [requirePermission("envoy.view")] }, async (request, reply) => {
|
|
195
|
+
const { id } = request.params as { id: string };
|
|
196
|
+
const entry = registry.get(id);
|
|
197
|
+
if (!entry) {
|
|
198
|
+
return reply.status(404).send({ error: "Envoy not found" });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const envoyDeployments = deployments.list().filter((d) => d.envoyId === id);
|
|
202
|
+
const observations: { id: string; timestamp: string; text: string }[] = [];
|
|
203
|
+
|
|
204
|
+
for (const d of envoyDeployments) {
|
|
205
|
+
const entries = debrief.getByDeployment(d.id);
|
|
206
|
+
for (const e of entries) {
|
|
207
|
+
if (e.decisionType === "environment-scan") {
|
|
208
|
+
observations.push({ id: e.id, timestamp: e.timestamp.toISOString(), text: e.reasoning || e.decision });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Sort newest first, cap at 20
|
|
214
|
+
observations.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
215
|
+
|
|
216
|
+
return { knowledge: observations.slice(0, 20) };
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Validate an Envoy token (used by Envoy report endpoint)
|
|
220
|
+
app.post("/api/envoys/validate-token", async (request, reply) => {
|
|
221
|
+
const body = request.body as { token: string };
|
|
222
|
+
if (!body.token) {
|
|
223
|
+
return reply.status(400).send({ error: "token is required" });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const envoy = registry.validateToken(body.token);
|
|
227
|
+
if (!envoy) {
|
|
228
|
+
return reply.status(401).send({ error: "Invalid token" });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
valid: true,
|
|
233
|
+
envoyId: envoy.id,
|
|
234
|
+
envoyName: envoy.name,
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
}
|