@synth-deploy/server 0.1.0 → 1.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/envoy-client.d.ts +62 -7
- package/dist/agent/envoy-client.d.ts.map +1 -1
- package/dist/agent/envoy-client.js +56 -6
- package/dist/agent/envoy-client.js.map +1 -1
- package/dist/agent/stale-deployment-detector.js +1 -1
- package/dist/agent/stale-deployment-detector.js.map +1 -1
- package/dist/agent/synth-agent.d.ts +7 -5
- package/dist/agent/synth-agent.d.ts.map +1 -1
- package/dist/agent/synth-agent.js +42 -39
- package/dist/agent/synth-agent.js.map +1 -1
- package/dist/alert-webhooks/alert-parsers.d.ts +21 -0
- package/dist/alert-webhooks/alert-parsers.d.ts.map +1 -0
- package/dist/alert-webhooks/alert-parsers.js +184 -0
- package/dist/alert-webhooks/alert-parsers.js.map +1 -0
- package/dist/api/agent.d.ts +0 -6
- package/dist/api/agent.d.ts.map +1 -1
- package/dist/api/agent.js +6 -459
- package/dist/api/agent.js.map +1 -1
- package/dist/api/alert-webhooks.d.ts +13 -0
- package/dist/api/alert-webhooks.d.ts.map +1 -0
- package/dist/api/alert-webhooks.js +279 -0
- package/dist/api/alert-webhooks.js.map +1 -0
- package/dist/api/envoy-reports.js +2 -2
- package/dist/api/envoy-reports.js.map +1 -1
- package/dist/api/envoys.js +1 -1
- package/dist/api/envoys.js.map +1 -1
- package/dist/api/fleet.d.ts.map +1 -1
- package/dist/api/fleet.js +14 -15
- package/dist/api/fleet.js.map +1 -1
- package/dist/api/graph.js +3 -3
- package/dist/api/graph.js.map +1 -1
- package/dist/api/operations.d.ts +7 -0
- package/dist/api/operations.d.ts.map +1 -0
- package/dist/api/operations.js +1883 -0
- package/dist/api/operations.js.map +1 -0
- package/dist/api/partitions.js +1 -1
- package/dist/api/partitions.js.map +1 -1
- package/dist/api/schemas.d.ts +194 -10
- package/dist/api/schemas.d.ts.map +1 -1
- package/dist/api/schemas.js +38 -5
- package/dist/api/schemas.js.map +1 -1
- package/dist/api/system.d.ts.map +1 -1
- package/dist/api/system.js +22 -21
- package/dist/api/system.js.map +1 -1
- package/dist/artifact-analyzer.js +2 -2
- package/dist/artifact-analyzer.js.map +1 -1
- package/dist/fleet/fleet-executor.js +1 -1
- package/dist/fleet/fleet-executor.js.map +1 -1
- package/dist/graph/graph-executor.js +2 -2
- package/dist/graph/graph-executor.js.map +1 -1
- package/dist/index.js +44 -40
- package/dist/index.js.map +1 -1
- package/dist/mcp/resources.js +3 -3
- package/dist/mcp/resources.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +2 -9
- package/dist/mcp/tools.js.map +1 -1
- package/dist/middleware/auth.js +1 -1
- package/dist/middleware/auth.js.map +1 -1
- package/package.json +1 -1
- package/src/agent/envoy-client.ts +107 -15
- package/src/agent/stale-deployment-detector.ts +1 -1
- package/src/agent/synth-agent.ts +59 -45
- package/src/alert-webhooks/alert-parsers.ts +291 -0
- package/src/api/agent.ts +9 -528
- package/src/api/alert-webhooks.ts +354 -0
- package/src/api/envoy-reports.ts +2 -2
- package/src/api/envoys.ts +1 -1
- package/src/api/fleet.ts +14 -15
- package/src/api/graph.ts +3 -3
- package/src/api/operations.ts +2240 -0
- package/src/api/partitions.ts +1 -1
- package/src/api/schemas.ts +43 -7
- package/src/api/system.ts +23 -21
- package/src/artifact-analyzer.ts +2 -2
- package/src/fleet/fleet-executor.ts +1 -1
- package/src/graph/graph-executor.ts +2 -2
- package/src/index.ts +46 -40
- package/src/mcp/resources.ts +3 -3
- package/src/mcp/tools.ts +5 -9
- package/src/middleware/auth.ts +1 -1
- package/tests/agent-mode.test.ts +5 -376
- package/tests/api-handlers.test.ts +27 -27
- package/tests/composite-operations.test.ts +557 -0
- package/tests/decision-diary.test.ts +62 -63
- package/tests/diary-reader.test.ts +14 -18
- package/tests/mcp-tools.test.ts +1 -1
- package/tests/orchestration.test.ts +34 -30
- package/tests/partition-isolation.test.ts +4 -9
- package/tests/rbac-enforcement.test.ts +8 -8
- package/tests/ui-journey.test.ts +9 -9
- package/dist/api/deployments.d.ts +0 -11
- package/dist/api/deployments.d.ts.map +0 -1
- package/dist/api/deployments.js +0 -1098
- package/dist/api/deployments.js.map +0 -1
- package/src/api/deployments.ts +0 -1347
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alert Webhook API routes — channel management + alert receipt endpoint.
|
|
3
|
+
*
|
|
4
|
+
* External monitoring systems (Prometheus AlertManager, PagerDuty, Datadog,
|
|
5
|
+
* Grafana) POST alerts here. Synth parses them, creates operations, and
|
|
6
|
+
* dispatches planning through the normal operation flow.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import crypto from "node:crypto";
|
|
10
|
+
import type { FastifyInstance } from "fastify";
|
|
11
|
+
import type {
|
|
12
|
+
IDeploymentStore,
|
|
13
|
+
IEnvironmentStore,
|
|
14
|
+
IPartitionStore,
|
|
15
|
+
ITelemetryStore,
|
|
16
|
+
DebriefWriter,
|
|
17
|
+
AlertWebhookSource,
|
|
18
|
+
} from "@synth-deploy/core";
|
|
19
|
+
import type { PersistentAlertWebhookStore } from "@synth-deploy/core";
|
|
20
|
+
import { requirePermission } from "../middleware/permissions.js";
|
|
21
|
+
import { parseAlerts, interpolateIntent } from "../alert-webhooks/alert-parsers.js";
|
|
22
|
+
import { EnvoyClient } from "../agent/envoy-client.js";
|
|
23
|
+
import type { EnvoyRegistry } from "../agent/envoy-registry.js";
|
|
24
|
+
|
|
25
|
+
const VALID_SOURCES: AlertWebhookSource[] = ["prometheus", "pagerduty", "datadog", "grafana", "generic"];
|
|
26
|
+
const VALID_OP_TYPES = ["maintain", "deploy", "query", "investigate"] as const;
|
|
27
|
+
|
|
28
|
+
export function registerAlertWebhookRoutes(
|
|
29
|
+
app: FastifyInstance,
|
|
30
|
+
store: PersistentAlertWebhookStore,
|
|
31
|
+
deployments: IDeploymentStore,
|
|
32
|
+
debrief: DebriefWriter,
|
|
33
|
+
environments: IEnvironmentStore,
|
|
34
|
+
partitions: IPartitionStore,
|
|
35
|
+
telemetry: ITelemetryStore,
|
|
36
|
+
envoyRegistry?: EnvoyRegistry,
|
|
37
|
+
): void {
|
|
38
|
+
|
|
39
|
+
// -----------------------------------------------------------------------
|
|
40
|
+
// Channel management — JWT-protected CRUD
|
|
41
|
+
// -----------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
app.get(
|
|
44
|
+
"/api/alert-webhooks",
|
|
45
|
+
{ preHandler: [requirePermission("settings.manage")] },
|
|
46
|
+
async () => {
|
|
47
|
+
const channels = store.list().map((ch) => ({
|
|
48
|
+
...ch,
|
|
49
|
+
// Never expose authToken in list responses
|
|
50
|
+
authToken: undefined,
|
|
51
|
+
}));
|
|
52
|
+
return { channels };
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
app.get<{ Params: { id: string } }>(
|
|
57
|
+
"/api/alert-webhooks/:id",
|
|
58
|
+
{ preHandler: [requirePermission("settings.manage")] },
|
|
59
|
+
async (request, reply) => {
|
|
60
|
+
const channel = store.get(request.params.id);
|
|
61
|
+
if (!channel) {
|
|
62
|
+
return reply.status(404).send({ error: "Alert webhook channel not found" });
|
|
63
|
+
}
|
|
64
|
+
return { channel: { ...channel, authToken: undefined } };
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
app.post(
|
|
69
|
+
"/api/alert-webhooks",
|
|
70
|
+
{ preHandler: [requirePermission("settings.manage")] },
|
|
71
|
+
async (request, reply) => {
|
|
72
|
+
const body = request.body as Record<string, unknown>;
|
|
73
|
+
const name = body.name as string;
|
|
74
|
+
const source = body.source as string;
|
|
75
|
+
const defaultOperationType = (body.defaultOperationType as string) ?? "maintain";
|
|
76
|
+
const defaultIntent = body.defaultIntent as string | undefined;
|
|
77
|
+
const environmentId = body.environmentId as string | undefined;
|
|
78
|
+
const partitionId = body.partitionId as string | undefined;
|
|
79
|
+
const envoyId = body.envoyId as string | undefined;
|
|
80
|
+
|
|
81
|
+
if (!name) {
|
|
82
|
+
return reply.status(400).send({ error: "name is required" });
|
|
83
|
+
}
|
|
84
|
+
if (!source || !VALID_SOURCES.includes(source as AlertWebhookSource)) {
|
|
85
|
+
return reply.status(400).send({ error: `source must be one of: ${VALID_SOURCES.join(", ")}` });
|
|
86
|
+
}
|
|
87
|
+
if (!VALID_OP_TYPES.includes(defaultOperationType as typeof VALID_OP_TYPES[number])) {
|
|
88
|
+
return reply.status(400).send({ error: `defaultOperationType must be one of: ${VALID_OP_TYPES.join(", ")}` });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const authToken = crypto.randomUUID();
|
|
92
|
+
const channel = store.create({
|
|
93
|
+
name,
|
|
94
|
+
source: source as AlertWebhookSource,
|
|
95
|
+
enabled: true,
|
|
96
|
+
authToken,
|
|
97
|
+
defaultOperationType: defaultOperationType as "maintain" | "deploy" | "query" | "investigate",
|
|
98
|
+
defaultIntent,
|
|
99
|
+
environmentId,
|
|
100
|
+
partitionId,
|
|
101
|
+
envoyId,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const actor = (request.user as { email?: string })?.email ?? "anonymous";
|
|
105
|
+
telemetry.record({
|
|
106
|
+
actor,
|
|
107
|
+
action: "alert-webhook.created",
|
|
108
|
+
target: { type: "alert-webhook", id: channel.id },
|
|
109
|
+
details: { source, name },
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Return the full channel including authToken (only on creation)
|
|
113
|
+
return reply.status(201).send({
|
|
114
|
+
channel,
|
|
115
|
+
webhookUrl: `/api/alert-webhooks/receive/${channel.id}`,
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
app.put<{ Params: { id: string } }>(
|
|
121
|
+
"/api/alert-webhooks/:id",
|
|
122
|
+
{ preHandler: [requirePermission("settings.manage")] },
|
|
123
|
+
async (request, reply) => {
|
|
124
|
+
const existing = store.get(request.params.id);
|
|
125
|
+
if (!existing) {
|
|
126
|
+
return reply.status(404).send({ error: "Alert webhook channel not found" });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const body = request.body as Record<string, unknown>;
|
|
130
|
+
const updates: Record<string, unknown> = {};
|
|
131
|
+
|
|
132
|
+
if (body.name !== undefined) updates.name = body.name;
|
|
133
|
+
if (body.source !== undefined) {
|
|
134
|
+
if (!VALID_SOURCES.includes(body.source as AlertWebhookSource)) {
|
|
135
|
+
return reply.status(400).send({ error: `source must be one of: ${VALID_SOURCES.join(", ")}` });
|
|
136
|
+
}
|
|
137
|
+
updates.source = body.source;
|
|
138
|
+
}
|
|
139
|
+
if (body.enabled !== undefined) updates.enabled = body.enabled;
|
|
140
|
+
if (body.defaultOperationType !== undefined) {
|
|
141
|
+
if (!VALID_OP_TYPES.includes(body.defaultOperationType as typeof VALID_OP_TYPES[number])) {
|
|
142
|
+
return reply.status(400).send({ error: `defaultOperationType must be one of: ${VALID_OP_TYPES.join(", ")}` });
|
|
143
|
+
}
|
|
144
|
+
updates.defaultOperationType = body.defaultOperationType;
|
|
145
|
+
}
|
|
146
|
+
if (body.defaultIntent !== undefined) updates.defaultIntent = body.defaultIntent;
|
|
147
|
+
if (body.environmentId !== undefined) updates.environmentId = body.environmentId;
|
|
148
|
+
if (body.partitionId !== undefined) updates.partitionId = body.partitionId;
|
|
149
|
+
if (body.envoyId !== undefined) updates.envoyId = body.envoyId;
|
|
150
|
+
|
|
151
|
+
const updated = store.update(request.params.id, updates);
|
|
152
|
+
return { channel: { ...updated, authToken: undefined } };
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
app.delete<{ Params: { id: string } }>(
|
|
157
|
+
"/api/alert-webhooks/:id",
|
|
158
|
+
{ preHandler: [requirePermission("settings.manage")] },
|
|
159
|
+
async (request, reply) => {
|
|
160
|
+
const deleted = store.delete(request.params.id);
|
|
161
|
+
if (!deleted) {
|
|
162
|
+
return reply.status(404).send({ error: "Alert webhook channel not found" });
|
|
163
|
+
}
|
|
164
|
+
return { deleted: true };
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// -----------------------------------------------------------------------
|
|
169
|
+
// Alert receipt — token-authenticated, JWT-exempt
|
|
170
|
+
// -----------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
app.post<{ Params: { channelId: string } }>(
|
|
173
|
+
"/api/alert-webhooks/receive/:channelId",
|
|
174
|
+
async (request, reply) => {
|
|
175
|
+
const channel = store.get(request.params.channelId);
|
|
176
|
+
if (!channel) {
|
|
177
|
+
return reply.status(404).send({ error: "Webhook channel not found" });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!channel.enabled) {
|
|
181
|
+
return reply.status(403).send({ error: "Webhook channel is disabled" });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Validate auth token from query parameter or header
|
|
185
|
+
const queryToken = (request.query as Record<string, string>).token;
|
|
186
|
+
const headerToken = request.headers["x-webhook-token"] as string | undefined;
|
|
187
|
+
const authHeader = request.headers.authorization;
|
|
188
|
+
const bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : undefined;
|
|
189
|
+
const token = queryToken || headerToken || bearerToken;
|
|
190
|
+
|
|
191
|
+
if (!token || token !== channel.authToken) {
|
|
192
|
+
return reply.status(401).send({ error: "Invalid or missing webhook token" });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Parse the alert payload
|
|
196
|
+
const alerts = parseAlerts(channel.source, request.body);
|
|
197
|
+
|
|
198
|
+
if (alerts.length === 0) {
|
|
199
|
+
return reply.status(200).send({
|
|
200
|
+
received: true,
|
|
201
|
+
operationsCreated: 0,
|
|
202
|
+
reason: "No firing alerts in payload (resolved alerts are ignored)",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const createdOps: string[] = [];
|
|
207
|
+
|
|
208
|
+
for (const alert of alerts) {
|
|
209
|
+
// Deduplication: check for active operations from this channel with the same alert name
|
|
210
|
+
const allOps = deployments.list();
|
|
211
|
+
const activeExisting = allOps.find(
|
|
212
|
+
(op) =>
|
|
213
|
+
op.lineage === `alert-webhook:${channel.id}` &&
|
|
214
|
+
op.intent?.includes(alert.name) &&
|
|
215
|
+
["pending", "planning", "awaiting_approval", "approved", "running"].includes(op.status),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
if (activeExisting) {
|
|
219
|
+
debrief.record({
|
|
220
|
+
partitionId: channel.partitionId ?? null,
|
|
221
|
+
operationId: activeExisting.id,
|
|
222
|
+
agent: "server",
|
|
223
|
+
decisionType: "alert-webhook-suppressed",
|
|
224
|
+
decision: `Alert "${alert.name}" suppressed — operation ${activeExisting.id} is already in progress (${activeExisting.status})`,
|
|
225
|
+
reasoning: `Deduplication: an operation for this alert from webhook channel "${channel.name}" is already active.`,
|
|
226
|
+
context: { channelId: channel.id, alertName: alert.name, activeOpId: activeExisting.id },
|
|
227
|
+
});
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Build intent from template or alert summary
|
|
232
|
+
const intent = channel.defaultIntent
|
|
233
|
+
? interpolateIntent(channel.defaultIntent, alert)
|
|
234
|
+
: `[${alert.severity.toUpperCase()}] ${alert.name}: ${alert.summary}`;
|
|
235
|
+
|
|
236
|
+
// Resolve environment/partition variables
|
|
237
|
+
const environment = channel.environmentId ? environments.get(channel.environmentId) : undefined;
|
|
238
|
+
const partition = channel.partitionId ? partitions.get(channel.partitionId) : undefined;
|
|
239
|
+
const envVars = environment?.variables ?? {};
|
|
240
|
+
const partitionVars = partition?.variables ?? {};
|
|
241
|
+
const resolved: Record<string, string> = { ...partitionVars, ...envVars };
|
|
242
|
+
|
|
243
|
+
const operationInput = channel.defaultOperationType === "deploy"
|
|
244
|
+
? { type: "deploy" as const, artifactId: "" }
|
|
245
|
+
: channel.defaultOperationType === "investigate"
|
|
246
|
+
? { type: "investigate" as const, intent }
|
|
247
|
+
: channel.defaultOperationType === "query"
|
|
248
|
+
? { type: "query" as const, intent }
|
|
249
|
+
: { type: "maintain" as const, intent };
|
|
250
|
+
|
|
251
|
+
const operation = {
|
|
252
|
+
id: crypto.randomUUID(),
|
|
253
|
+
input: operationInput,
|
|
254
|
+
intent,
|
|
255
|
+
lineage: `alert-webhook:${channel.id}`,
|
|
256
|
+
triggeredBy: "webhook" as const,
|
|
257
|
+
environmentId: channel.environmentId,
|
|
258
|
+
partitionId: channel.partitionId,
|
|
259
|
+
envoyId: channel.envoyId,
|
|
260
|
+
version: "",
|
|
261
|
+
status: "pending" as const,
|
|
262
|
+
variables: resolved,
|
|
263
|
+
debriefEntryIds: [] as string[],
|
|
264
|
+
createdAt: new Date(),
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
deployments.save(operation);
|
|
268
|
+
createdOps.push(operation.id);
|
|
269
|
+
|
|
270
|
+
// Record the alert receipt in debrief
|
|
271
|
+
debrief.record({
|
|
272
|
+
partitionId: channel.partitionId ?? null,
|
|
273
|
+
operationId: operation.id,
|
|
274
|
+
agent: "server",
|
|
275
|
+
decisionType: "alert-webhook-received",
|
|
276
|
+
decision: `External alert received from ${channel.source}: "${alert.name}" (${alert.severity})`,
|
|
277
|
+
reasoning: `Webhook channel "${channel.name}" received a ${alert.severity} alert. Intent: ${intent}`,
|
|
278
|
+
context: {
|
|
279
|
+
channelId: channel.id,
|
|
280
|
+
channelName: channel.name,
|
|
281
|
+
source: channel.source,
|
|
282
|
+
alertName: alert.name,
|
|
283
|
+
alertSeverity: alert.severity,
|
|
284
|
+
alertLabels: alert.labels,
|
|
285
|
+
rawPayload: alert.rawPayload,
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
telemetry.record({
|
|
290
|
+
actor: `webhook:${channel.name}`,
|
|
291
|
+
action: "alert-webhook.fired",
|
|
292
|
+
target: { type: "deployment" as const, id: operation.id },
|
|
293
|
+
details: { channelId: channel.id, alertName: alert.name, severity: alert.severity },
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Dispatch planning to an envoy
|
|
297
|
+
if (envoyRegistry) {
|
|
298
|
+
const targetEnvoy = channel.envoyId
|
|
299
|
+
? envoyRegistry.get(channel.envoyId)
|
|
300
|
+
: environment
|
|
301
|
+
? envoyRegistry.findForEnvironment(environment.name)
|
|
302
|
+
: envoyRegistry.list()[0];
|
|
303
|
+
|
|
304
|
+
if (targetEnvoy) {
|
|
305
|
+
const planningClient = new EnvoyClient(targetEnvoy.url);
|
|
306
|
+
const environmentForPlanning = environment
|
|
307
|
+
? { id: environment.id, name: environment.name, variables: environment.variables }
|
|
308
|
+
: { id: `direct:${targetEnvoy.id}`, name: targetEnvoy.name, variables: {} };
|
|
309
|
+
|
|
310
|
+
planningClient.requestPlan({
|
|
311
|
+
operationId: operation.id,
|
|
312
|
+
operationType: channel.defaultOperationType as "deploy" | "query" | "investigate" | "maintain",
|
|
313
|
+
intent,
|
|
314
|
+
environment: environmentForPlanning,
|
|
315
|
+
partition: partition
|
|
316
|
+
? { id: partition.id, name: partition.name, variables: partition.variables }
|
|
317
|
+
: undefined,
|
|
318
|
+
version: "",
|
|
319
|
+
resolvedVariables: resolved,
|
|
320
|
+
}).then((result) => {
|
|
321
|
+
const dep = deployments.get(operation.id);
|
|
322
|
+
if (!dep || dep.status !== "pending") return;
|
|
323
|
+
|
|
324
|
+
dep.plan = result.plan;
|
|
325
|
+
dep.rollbackPlan = result.rollbackPlan;
|
|
326
|
+
dep.envoyId = targetEnvoy.id;
|
|
327
|
+
|
|
328
|
+
if (result.blocked) {
|
|
329
|
+
dep.status = "failed" as typeof dep.status;
|
|
330
|
+
dep.failureReason = result.blockReason ?? "Plan blocked";
|
|
331
|
+
deployments.save(dep);
|
|
332
|
+
} else {
|
|
333
|
+
dep.status = "awaiting_approval" as typeof dep.status;
|
|
334
|
+
deployments.save(dep);
|
|
335
|
+
}
|
|
336
|
+
}).catch((err) => {
|
|
337
|
+
const dep = deployments.get(operation.id);
|
|
338
|
+
if (!dep || dep.status !== "pending") return;
|
|
339
|
+
dep.status = "failed" as typeof dep.status;
|
|
340
|
+
dep.failureReason = err instanceof Error ? err.message : "Planning failed";
|
|
341
|
+
deployments.save(dep);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return reply.status(201).send({
|
|
348
|
+
received: true,
|
|
349
|
+
operationsCreated: createdOps.length,
|
|
350
|
+
operationIds: createdOps,
|
|
351
|
+
});
|
|
352
|
+
},
|
|
353
|
+
);
|
|
354
|
+
}
|
package/src/api/envoy-reports.ts
CHANGED
|
@@ -90,7 +90,7 @@ export function registerEnvoyReportRoutes(
|
|
|
90
90
|
if (!deployment || deployment.partitionId !== entry.partitionId) {
|
|
91
91
|
debrief.record({
|
|
92
92
|
partitionId: entry.partitionId,
|
|
93
|
-
|
|
93
|
+
operationId: entry.deploymentId,
|
|
94
94
|
agent: "server",
|
|
95
95
|
decisionType: "system",
|
|
96
96
|
decision: "Rejected Envoy report: partition boundary violation",
|
|
@@ -114,7 +114,7 @@ export function registerEnvoyReportRoutes(
|
|
|
114
114
|
for (const entry of report.debriefEntries) {
|
|
115
115
|
debrief.record({
|
|
116
116
|
partitionId: entry.partitionId,
|
|
117
|
-
|
|
117
|
+
operationId: entry.deploymentId,
|
|
118
118
|
agent: entry.agent as "server" | "envoy",
|
|
119
119
|
decisionType: entry.decisionType as DecisionType,
|
|
120
120
|
decision: entry.decision,
|
package/src/api/envoys.ts
CHANGED
|
@@ -202,7 +202,7 @@ export function registerEnvoyRoutes(
|
|
|
202
202
|
const observations: { id: string; timestamp: string; text: string }[] = [];
|
|
203
203
|
|
|
204
204
|
for (const d of envoyDeployments) {
|
|
205
|
-
const entries = debrief.
|
|
205
|
+
const entries = debrief.getByOperation(d.id);
|
|
206
206
|
for (const e of entries) {
|
|
207
207
|
if (e.decisionType === "environment-scan") {
|
|
208
208
|
observations.push({ id: e.id, timestamp: e.timestamp.toISOString(), text: e.reasoning || e.decision });
|
package/src/api/fleet.ts
CHANGED
|
@@ -97,7 +97,7 @@ export function registerFleetRoutes(
|
|
|
97
97
|
|
|
98
98
|
debrief.record({
|
|
99
99
|
partitionId: null,
|
|
100
|
-
|
|
100
|
+
operationId: fleetDeployment.id,
|
|
101
101
|
agent: "server",
|
|
102
102
|
decisionType: "system",
|
|
103
103
|
decision: `Fleet deployment created for ${targetEnvoys.length} envoys with ${rolloutConfig.strategy} strategy`,
|
|
@@ -143,8 +143,7 @@ export function registerFleetRoutes(
|
|
|
143
143
|
|
|
144
144
|
const deployment: Deployment = {
|
|
145
145
|
id: crypto.randomUUID(),
|
|
146
|
-
artifactId: fleet.artifactId,
|
|
147
|
-
artifactVersionId: fleet.artifactVersionId,
|
|
146
|
+
input: { type: "deploy" as const, artifactId: fleet.artifactId, ...(fleet.artifactVersionId ? { artifactVersionId: fleet.artifactVersionId } : {}) },
|
|
148
147
|
envoyId: targetEnvoyId,
|
|
149
148
|
environmentId: fleet.environmentId,
|
|
150
149
|
version: "",
|
|
@@ -162,7 +161,7 @@ export function registerFleetRoutes(
|
|
|
162
161
|
|
|
163
162
|
debrief.record({
|
|
164
163
|
partitionId: null,
|
|
165
|
-
|
|
164
|
+
operationId: fleet.id,
|
|
166
165
|
agent: "server",
|
|
167
166
|
decisionType: "system",
|
|
168
167
|
decision: `Representative plan created for envoy ${targetEnvoyId}`,
|
|
@@ -240,7 +239,7 @@ export function registerFleetRoutes(
|
|
|
240
239
|
|
|
241
240
|
debrief.record({
|
|
242
241
|
partitionId: null,
|
|
243
|
-
|
|
242
|
+
operationId: fleet.id,
|
|
244
243
|
agent: "server",
|
|
245
244
|
decisionType: "system",
|
|
246
245
|
decision: `Fleet deployment approved by ${actor}, starting fleet validation`,
|
|
@@ -262,7 +261,7 @@ export function registerFleetRoutes(
|
|
|
262
261
|
|
|
263
262
|
debrief.record({
|
|
264
263
|
partitionId: null,
|
|
265
|
-
|
|
264
|
+
operationId: fleet.id,
|
|
266
265
|
agent: "server",
|
|
267
266
|
decisionType: "system",
|
|
268
267
|
decision: `Fleet validation complete: ${validationResult.validated}/${validationResult.total} envoys passed`,
|
|
@@ -278,7 +277,7 @@ export function registerFleetRoutes(
|
|
|
278
277
|
|
|
279
278
|
debrief.record({
|
|
280
279
|
partitionId: null,
|
|
281
|
-
|
|
280
|
+
operationId: fleet.id,
|
|
282
281
|
agent: "server",
|
|
283
282
|
decisionType: "deployment-failure",
|
|
284
283
|
decision: "Fleet validation failed unexpectedly",
|
|
@@ -328,7 +327,7 @@ export function registerFleetRoutes(
|
|
|
328
327
|
|
|
329
328
|
debrief.record({
|
|
330
329
|
partitionId: null,
|
|
331
|
-
|
|
330
|
+
operationId: fleet.id,
|
|
332
331
|
agent: "server",
|
|
333
332
|
decisionType: "system",
|
|
334
333
|
decision: `Fleet rollout started by ${actor}`,
|
|
@@ -359,7 +358,7 @@ export function registerFleetRoutes(
|
|
|
359
358
|
if (event.type === "envoy-failed") {
|
|
360
359
|
debrief.record({
|
|
361
360
|
partitionId: null,
|
|
362
|
-
|
|
361
|
+
operationId: fleet.id,
|
|
363
362
|
agent: "server",
|
|
364
363
|
decisionType: "deployment-failure",
|
|
365
364
|
decision: `Envoy ${event.envoyName ?? event.envoyId} failed during fleet rollout`,
|
|
@@ -369,7 +368,7 @@ export function registerFleetRoutes(
|
|
|
369
368
|
} else if (event.type === "fleet-completed") {
|
|
370
369
|
debrief.record({
|
|
371
370
|
partitionId: null,
|
|
372
|
-
|
|
371
|
+
operationId: fleet.id,
|
|
373
372
|
agent: "server",
|
|
374
373
|
decisionType: "deployment-completion",
|
|
375
374
|
decision: `Fleet rollout completed: ${event.progress.succeeded}/${event.progress.totalEnvoys} succeeded`,
|
|
@@ -379,7 +378,7 @@ export function registerFleetRoutes(
|
|
|
379
378
|
} else if (event.type === "fleet-failed") {
|
|
380
379
|
debrief.record({
|
|
381
380
|
partitionId: null,
|
|
382
|
-
|
|
381
|
+
operationId: fleet.id,
|
|
383
382
|
agent: "server",
|
|
384
383
|
decisionType: "deployment-failure",
|
|
385
384
|
decision: `Fleet rollout halted: failure threshold reached`,
|
|
@@ -394,7 +393,7 @@ export function registerFleetRoutes(
|
|
|
394
393
|
const durationSec = Math.round(durationMs / 1000);
|
|
395
394
|
debrief.record({
|
|
396
395
|
partitionId: null,
|
|
397
|
-
|
|
396
|
+
operationId: fleet.id,
|
|
398
397
|
agent: "server",
|
|
399
398
|
decisionType: "deployment-completion",
|
|
400
399
|
decision: `Fleet deployment ${fleet.status}: ${fleet.progress.succeeded}/${fleet.progress.totalEnvoys} envoys succeeded, ${fleet.progress.failed} failed`,
|
|
@@ -417,7 +416,7 @@ export function registerFleetRoutes(
|
|
|
417
416
|
|
|
418
417
|
debrief.record({
|
|
419
418
|
partitionId: null,
|
|
420
|
-
|
|
419
|
+
operationId: fleet.id,
|
|
421
420
|
agent: "server",
|
|
422
421
|
decisionType: "deployment-failure",
|
|
423
422
|
decision: `Fleet rollout failed with unexpected error after ${durationSec}s`,
|
|
@@ -456,7 +455,7 @@ export function registerFleetRoutes(
|
|
|
456
455
|
|
|
457
456
|
debrief.record({
|
|
458
457
|
partitionId: null,
|
|
459
|
-
|
|
458
|
+
operationId: fleet.id,
|
|
460
459
|
agent: "server",
|
|
461
460
|
decisionType: "system",
|
|
462
461
|
decision: `Fleet rollout paused by ${actor}`,
|
|
@@ -495,7 +494,7 @@ export function registerFleetRoutes(
|
|
|
495
494
|
|
|
496
495
|
debrief.record({
|
|
497
496
|
partitionId: null,
|
|
498
|
-
|
|
497
|
+
operationId: fleet.id,
|
|
499
498
|
agent: "server",
|
|
500
499
|
decisionType: "system",
|
|
501
500
|
decision: `Fleet rollout resumed by ${actor}`,
|
package/src/api/graph.ts
CHANGED
|
@@ -83,7 +83,7 @@ export function registerGraphRoutes(
|
|
|
83
83
|
// Record debrief entry for graph creation with inference reasoning
|
|
84
84
|
debrief.record({
|
|
85
85
|
partitionId: graph.partitionId ?? null,
|
|
86
|
-
|
|
86
|
+
operationId: null,
|
|
87
87
|
agent: "server",
|
|
88
88
|
decisionType: "plan-generation",
|
|
89
89
|
decision: `Created deployment graph "${graph.name}" with ${graph.nodes.length} nodes and ${graph.edges.length} edges`,
|
|
@@ -166,7 +166,7 @@ export function registerGraphRoutes(
|
|
|
166
166
|
// Record debrief entry for user corrections
|
|
167
167
|
debrief.record({
|
|
168
168
|
partitionId: graph.partitionId ?? null,
|
|
169
|
-
|
|
169
|
+
operationId: null,
|
|
170
170
|
agent: "server",
|
|
171
171
|
decisionType: "plan-modification",
|
|
172
172
|
decision: `User corrected deployment graph "${graph.name}"`,
|
|
@@ -308,7 +308,7 @@ export function registerGraphRoutes(
|
|
|
308
308
|
|
|
309
309
|
debrief.record({
|
|
310
310
|
partitionId: graph.partitionId ?? null,
|
|
311
|
-
|
|
311
|
+
operationId: null,
|
|
312
312
|
agent: "server",
|
|
313
313
|
decisionType:
|
|
314
314
|
finalFailedCount > 0 ? "deployment-failure" : "deployment-completion",
|