@synth-deploy/server 1.1.0 → 1.2.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 +5 -10
- package/dist/agent/envoy-client.d.ts.map +1 -1
- package/dist/agent/envoy-client.js +4 -4
- package/dist/agent/envoy-client.js.map +1 -1
- package/dist/agent/synth-agent.d.ts.map +1 -1
- package/dist/agent/synth-agent.js +17 -11
- package/dist/agent/synth-agent.js.map +1 -1
- package/dist/api/operations.d.ts.map +1 -1
- package/dist/api/operations.js +91 -74
- package/dist/api/operations.js.map +1 -1
- package/dist/api/schemas.d.ts +250 -133
- package/dist/api/schemas.d.ts.map +1 -1
- package/dist/api/schemas.js +17 -22
- package/dist/api/schemas.js.map +1 -1
- package/dist/fleet/fleet-executor.js +2 -2
- package/dist/fleet/fleet-executor.js.map +1 -1
- package/dist/graph/graph-executor.d.ts.map +1 -1
- package/dist/graph/graph-executor.js +16 -2
- package/dist/graph/graph-executor.js.map +1 -1
- package/dist/index.js +45 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agent/envoy-client.ts +8 -8
- package/src/agent/synth-agent.ts +17 -11
- package/src/api/operations.ts +94 -74
- package/src/api/schemas.ts +18 -22
- package/src/fleet/fleet-executor.ts +2 -2
- package/src/graph/graph-executor.ts +16 -2
- package/src/index.ts +45 -21
- package/tests/composite-operations.test.ts +6 -6
package/src/api/operations.ts
CHANGED
|
@@ -183,17 +183,13 @@ export function registerOperationRoutes(
|
|
|
183
183
|
|
|
184
184
|
// Trigger operations: construct MonitoringDirective from plan, present for approval
|
|
185
185
|
if (dep.input.type === "trigger" && !result.blocked) {
|
|
186
|
-
const triggerInput = dep.input as { type: "trigger"; condition: string; responseIntent: string
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
command: step.action,
|
|
190
|
-
label: step.description,
|
|
191
|
-
parseAs: (step.params?.parseAs === "exitCode" ? "exitCode" : "numeric") as "numeric" | "exitCode",
|
|
192
|
-
}));
|
|
186
|
+
const triggerInput = dep.input as { type: "trigger"; condition: string; responseIntent: string };
|
|
187
|
+
// Use probes from the envoy's trigger planning response (embedded in scriptedPlan reasoning),
|
|
188
|
+
// or fall back to a default probe. The envoy's planTrigger generates these.
|
|
193
189
|
const directive: import("@synth-deploy/core").MonitoringDirective = {
|
|
194
190
|
id: dep.id,
|
|
195
191
|
operationId: dep.id,
|
|
196
|
-
probes:
|
|
192
|
+
probes: [{
|
|
197
193
|
command: "echo 0",
|
|
198
194
|
label: "default-probe",
|
|
199
195
|
parseAs: "numeric" as const,
|
|
@@ -203,7 +199,6 @@ export function registerOperationRoutes(
|
|
|
203
199
|
condition: triggerInput.condition,
|
|
204
200
|
responseIntent: triggerInput.responseIntent,
|
|
205
201
|
responseType: "maintain",
|
|
206
|
-
responseParameters: triggerInput.parameters,
|
|
207
202
|
environmentId: dep.environmentId,
|
|
208
203
|
partitionId: dep.partitionId,
|
|
209
204
|
status: "active",
|
|
@@ -266,7 +261,7 @@ export function registerOperationRoutes(
|
|
|
266
261
|
decisionType: "plan-generation",
|
|
267
262
|
decision: `Operation plan blocked — infrastructure prerequisites not met`,
|
|
268
263
|
reasoning: result.blockReason ?? result.plan.reasoning,
|
|
269
|
-
context: { stepCount: result.plan.
|
|
264
|
+
context: { stepCount: result.plan.scriptedPlan.stepSummary.length, envoyId: planningEnvoy.id, blocked: true },
|
|
270
265
|
});
|
|
271
266
|
} else {
|
|
272
267
|
// Plan is valid — transition to awaiting_approval
|
|
@@ -279,9 +274,9 @@ export function registerOperationRoutes(
|
|
|
279
274
|
operationId: dep.id,
|
|
280
275
|
agent: "envoy",
|
|
281
276
|
decisionType: "plan-generation",
|
|
282
|
-
decision: `Operation plan generated with ${result.plan.
|
|
277
|
+
decision: `Operation plan generated with ${result.plan.scriptedPlan.stepSummary.length} steps`,
|
|
283
278
|
reasoning: result.plan.reasoning,
|
|
284
|
-
context: { stepCount: result.plan.
|
|
279
|
+
context: { stepCount: result.plan.scriptedPlan.stepSummary.length, envoyId: planningEnvoy.id, delta: result.delta },
|
|
285
280
|
});
|
|
286
281
|
}
|
|
287
282
|
}).catch((err) => {
|
|
@@ -400,9 +395,9 @@ export function registerOperationRoutes(
|
|
|
400
395
|
operationId: deployment.id,
|
|
401
396
|
agent: "envoy",
|
|
402
397
|
decisionType: "plan-generation",
|
|
403
|
-
decision: `Operation plan submitted with ${parsed.data.plan.
|
|
398
|
+
decision: `Operation plan submitted with ${parsed.data.plan.scriptedPlan.stepSummary.length} steps`,
|
|
404
399
|
reasoning: parsed.data.plan.reasoning,
|
|
405
|
-
context: { stepCount: parsed.data.plan.
|
|
400
|
+
context: { stepCount: parsed.data.plan.scriptedPlan.stepSummary.length },
|
|
406
401
|
});
|
|
407
402
|
|
|
408
403
|
return reply.status(200).send({ deployment });
|
|
@@ -456,7 +451,7 @@ export function registerOperationRoutes(
|
|
|
456
451
|
target: { type: "deployment", id: deployment.id },
|
|
457
452
|
details: parsed.data.modifications
|
|
458
453
|
? { modifications: parsed.data.modifications }
|
|
459
|
-
: { planStepCount: deployment.plan?.
|
|
454
|
+
: { planStepCount: deployment.plan?.scriptedPlan.stepSummary.length ?? 0 },
|
|
460
455
|
});
|
|
461
456
|
|
|
462
457
|
// Composite operations: execute children sequentially
|
|
@@ -644,9 +639,14 @@ export function registerOperationRoutes(
|
|
|
644
639
|
}
|
|
645
640
|
|
|
646
641
|
// Validate modified plan with envoy if available
|
|
647
|
-
if (envoyClient) {
|
|
642
|
+
if (envoyClient && deployment.plan.scriptedPlan) {
|
|
648
643
|
try {
|
|
649
|
-
const
|
|
644
|
+
const modifiedScript: import("@synth-deploy/core").ScriptedPlan = {
|
|
645
|
+
...deployment.plan.scriptedPlan,
|
|
646
|
+
executionScript: parsed.data.executionScript,
|
|
647
|
+
...(parsed.data.rollbackScript !== undefined ? { rollbackScript: parsed.data.rollbackScript } : {}),
|
|
648
|
+
};
|
|
649
|
+
const validation = await envoyClient.validatePlan(modifiedScript);
|
|
650
650
|
if (!validation.valid) {
|
|
651
651
|
return reply.status(422).send({
|
|
652
652
|
error: "Modified plan failed envoy validation",
|
|
@@ -658,34 +658,21 @@ export function registerOperationRoutes(
|
|
|
658
658
|
}
|
|
659
659
|
}
|
|
660
660
|
|
|
661
|
-
//
|
|
662
|
-
const
|
|
663
|
-
const
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
const old = oldSteps[i];
|
|
668
|
-
const cur = newSteps[i];
|
|
669
|
-
if (!old) {
|
|
670
|
-
diffLines.push(`+ Step ${i + 1} (added): ${cur.action} ${cur.target} — ${cur.description}`);
|
|
671
|
-
} else if (!cur) {
|
|
672
|
-
diffLines.push(`- Step ${i + 1} (removed): ${old.action} ${old.target} — ${old.description}`);
|
|
673
|
-
} else if (old.action !== cur.action || old.target !== cur.target || old.description !== cur.description) {
|
|
674
|
-
diffLines.push(`~ Step ${i + 1} (changed): ${old.action} ${old.target} → ${cur.action} ${cur.target}`);
|
|
675
|
-
if (old.description !== cur.description) {
|
|
676
|
-
diffLines.push(` was: ${old.description}`);
|
|
677
|
-
diffLines.push(` now: ${cur.description}`);
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
const diffFromPreviousPlan = diffLines.length > 0
|
|
682
|
-
? diffLines.join("\n")
|
|
683
|
-
: "Steps reordered or metadata changed (actions and targets unchanged)";
|
|
661
|
+
// Compute diff description
|
|
662
|
+
const oldScript = deployment.plan.scriptedPlan?.executionScript ?? "";
|
|
663
|
+
const newScript = parsed.data.executionScript;
|
|
664
|
+
const diffFromPreviousPlan = oldScript !== newScript
|
|
665
|
+
? "Execution script modified by user"
|
|
666
|
+
: "Plan metadata changed (script unchanged)";
|
|
684
667
|
|
|
685
668
|
// Apply modifications
|
|
686
669
|
deployment.plan = {
|
|
687
670
|
...deployment.plan,
|
|
688
|
-
|
|
671
|
+
scriptedPlan: {
|
|
672
|
+
...deployment.plan.scriptedPlan,
|
|
673
|
+
executionScript: parsed.data.executionScript,
|
|
674
|
+
...(parsed.data.rollbackScript !== undefined ? { rollbackScript: parsed.data.rollbackScript } : {}),
|
|
675
|
+
},
|
|
689
676
|
diffFromPreviousPlan,
|
|
690
677
|
};
|
|
691
678
|
deployments.save(deployment);
|
|
@@ -702,7 +689,6 @@ export function registerOperationRoutes(
|
|
|
702
689
|
reasoning: parsed.data.reason,
|
|
703
690
|
context: {
|
|
704
691
|
modifiedBy: actor,
|
|
705
|
-
stepCount: parsed.data.steps.length,
|
|
706
692
|
reason: parsed.data.reason,
|
|
707
693
|
},
|
|
708
694
|
actor: request.user?.email,
|
|
@@ -711,13 +697,13 @@ export function registerOperationRoutes(
|
|
|
711
697
|
actor,
|
|
712
698
|
action: "operation.modified" as Parameters<typeof telemetry.record>[0]["action"],
|
|
713
699
|
target: { type: "deployment", id: deployment.id },
|
|
714
|
-
details: { reason: parsed.data.reason
|
|
700
|
+
details: { reason: parsed.data.reason },
|
|
715
701
|
});
|
|
716
702
|
telemetry.record({
|
|
717
703
|
actor,
|
|
718
704
|
action: "agent.recommendation.overridden",
|
|
719
705
|
target: { type: "deployment", id: deployment.id },
|
|
720
|
-
details: { reason: parsed.data.reason,
|
|
706
|
+
details: { reason: parsed.data.reason, diff: diffFromPreviousPlan },
|
|
721
707
|
});
|
|
722
708
|
|
|
723
709
|
return { deployment, modified: true };
|
|
@@ -762,10 +748,9 @@ export function registerOperationRoutes(
|
|
|
762
748
|
try {
|
|
763
749
|
const validation = await planningClientForValidation.validateRefinementFeedback({
|
|
764
750
|
feedback: parsed.data.feedback,
|
|
765
|
-
|
|
751
|
+
currentPlanSummary: (deployment.plan?.scriptedPlan?.stepSummary ?? []).map((s) => ({
|
|
766
752
|
description: s.description,
|
|
767
|
-
|
|
768
|
-
target: s.target,
|
|
753
|
+
reversible: s.reversible,
|
|
769
754
|
})),
|
|
770
755
|
artifactName: artifact?.name ?? "unknown",
|
|
771
756
|
environmentName: environment?.name ?? "unknown",
|
|
@@ -838,9 +823,9 @@ export function registerOperationRoutes(
|
|
|
838
823
|
operationId: dep.id,
|
|
839
824
|
agent: "envoy",
|
|
840
825
|
decisionType: "plan-generation",
|
|
841
|
-
decision: `Plan regenerated with user feedback (${result.plan.
|
|
826
|
+
decision: `Plan regenerated with user feedback (${result.plan.scriptedPlan.stepSummary.length} steps)`,
|
|
842
827
|
reasoning: result.plan.reasoning,
|
|
843
|
-
context: { stepCount: result.plan.
|
|
828
|
+
context: { stepCount: result.plan.scriptedPlan.stepSummary.length, envoyId: planningEnvoy.id, refinementFeedback: parsed.data.feedback },
|
|
844
829
|
});
|
|
845
830
|
|
|
846
831
|
return { deployment: dep, replanned: true };
|
|
@@ -947,7 +932,7 @@ export function registerOperationRoutes(
|
|
|
947
932
|
|
|
948
933
|
const environment = deployment.environmentId ? environments.get(deployment.environmentId) : undefined;
|
|
949
934
|
|
|
950
|
-
// Build the list of completed steps from execution record (or plan as fallback)
|
|
935
|
+
// Build the list of completed steps from execution record (or plan step summaries as fallback)
|
|
951
936
|
const completedSteps: Array<{
|
|
952
937
|
description: string;
|
|
953
938
|
action: string;
|
|
@@ -956,14 +941,14 @@ export function registerOperationRoutes(
|
|
|
956
941
|
output?: string;
|
|
957
942
|
}> = deployment.executionRecord?.steps.map((s) => ({
|
|
958
943
|
description: s.description,
|
|
959
|
-
action:
|
|
960
|
-
target:
|
|
944
|
+
action: "script-step",
|
|
945
|
+
target: "",
|
|
961
946
|
status: s.status,
|
|
962
947
|
output: s.output ?? s.error,
|
|
963
|
-
})) ?? deployment.plan?.
|
|
948
|
+
})) ?? deployment.plan?.scriptedPlan?.stepSummary.map((s) => ({
|
|
964
949
|
description: s.description,
|
|
965
|
-
action:
|
|
966
|
-
target:
|
|
950
|
+
action: "script-step",
|
|
951
|
+
target: "",
|
|
967
952
|
status: "completed" as const,
|
|
968
953
|
})) ?? [];
|
|
969
954
|
|
|
@@ -1008,7 +993,7 @@ export function registerOperationRoutes(
|
|
|
1008
993
|
reasoning: rollbackPlan.reasoning,
|
|
1009
994
|
context: {
|
|
1010
995
|
requestedBy: actor,
|
|
1011
|
-
stepCount: rollbackPlan.
|
|
996
|
+
stepCount: rollbackPlan.scriptedPlan.stepSummary.length,
|
|
1012
997
|
envoyId: targetEnvoy.id,
|
|
1013
998
|
deploymentStatus: deployment.status,
|
|
1014
999
|
},
|
|
@@ -1018,7 +1003,7 @@ export function registerOperationRoutes(
|
|
|
1018
1003
|
actor,
|
|
1019
1004
|
action: "deployment.rollback-plan-requested" as Parameters<typeof telemetry.record>[0]["action"],
|
|
1020
1005
|
target: { type: "deployment", id: deployment.id },
|
|
1021
|
-
details: { stepCount: rollbackPlan.
|
|
1006
|
+
details: { stepCount: rollbackPlan.scriptedPlan.stepSummary.length },
|
|
1022
1007
|
});
|
|
1023
1008
|
|
|
1024
1009
|
return reply.status(200).send({ deployment, rollbackPlan });
|
|
@@ -1075,22 +1060,32 @@ export function registerOperationRoutes(
|
|
|
1075
1060
|
agent: "server",
|
|
1076
1061
|
decisionType: "rollback-execution",
|
|
1077
1062
|
decision: `Rollback execution initiated for ${artifact?.name ?? getArtifactId(deployment)} v${deployment.version}`,
|
|
1078
|
-
reasoning: `Rollback requested by ${actor}. Executing ${deployment.rollbackPlan.
|
|
1079
|
-
context: { initiatedBy: actor, stepCount: deployment.rollbackPlan.
|
|
1063
|
+
reasoning: `Rollback requested by ${actor}. Executing ${deployment.rollbackPlan.scriptedPlan.stepSummary.length} rollback step(s).`,
|
|
1064
|
+
context: { initiatedBy: actor, stepCount: deployment.rollbackPlan.scriptedPlan.stepSummary.length },
|
|
1080
1065
|
actor: request.user?.email,
|
|
1081
1066
|
});
|
|
1082
1067
|
telemetry.record({
|
|
1083
1068
|
actor,
|
|
1084
1069
|
action: "deployment.rollback-executed" as Parameters<typeof telemetry.record>[0]["action"],
|
|
1085
1070
|
target: { type: "deployment", id: deployment.id },
|
|
1086
|
-
details: { stepCount: deployment.rollbackPlan.
|
|
1071
|
+
details: { stepCount: deployment.rollbackPlan.scriptedPlan.stepSummary.length },
|
|
1087
1072
|
});
|
|
1088
1073
|
|
|
1089
1074
|
const rollbackClient = new EnvoyClient(targetEnvoy.url);
|
|
1090
1075
|
|
|
1091
1076
|
// Execute the rollback plan as if it were a forward plan — it IS a forward plan
|
|
1092
1077
|
// (just in the reverse direction). Use an empty no-op plan as the "rollback of rollback".
|
|
1093
|
-
const emptyPlan
|
|
1078
|
+
const emptyPlan: import("@synth-deploy/core").OperationPlan = {
|
|
1079
|
+
scriptedPlan: {
|
|
1080
|
+
platform: "bash",
|
|
1081
|
+
executionScript: "# No rollback of rollback",
|
|
1082
|
+
dryRunScript: null,
|
|
1083
|
+
rollbackScript: null,
|
|
1084
|
+
reasoning: "No rollback of rollback.",
|
|
1085
|
+
stepSummary: [],
|
|
1086
|
+
},
|
|
1087
|
+
reasoning: "No rollback of rollback.",
|
|
1088
|
+
};
|
|
1094
1089
|
|
|
1095
1090
|
rollbackClient.executeApprovedPlan({
|
|
1096
1091
|
operationId: deployment.id,
|
|
@@ -1269,7 +1264,7 @@ export function registerOperationRoutes(
|
|
|
1269
1264
|
decisionType: "plan-generation",
|
|
1270
1265
|
decision: `Operation plan blocked — infrastructure prerequisites not met`,
|
|
1271
1266
|
reasoning: result.blockReason ?? result.plan.reasoning,
|
|
1272
|
-
context: { stepCount: result.plan.
|
|
1267
|
+
context: { stepCount: result.plan.scriptedPlan.stepSummary.length, envoyId: planningEnvoy.id, blocked: true },
|
|
1273
1268
|
});
|
|
1274
1269
|
} else {
|
|
1275
1270
|
dep.status = "awaiting_approval" as typeof dep.status;
|
|
@@ -1281,9 +1276,9 @@ export function registerOperationRoutes(
|
|
|
1281
1276
|
operationId: dep.id,
|
|
1282
1277
|
agent: "envoy",
|
|
1283
1278
|
decisionType: "plan-generation",
|
|
1284
|
-
decision: `Operation plan generated with ${result.plan.
|
|
1279
|
+
decision: `Operation plan generated with ${result.plan.scriptedPlan.stepSummary.length} steps`,
|
|
1285
1280
|
reasoning: result.plan.reasoning,
|
|
1286
|
-
context: { stepCount: result.plan.
|
|
1281
|
+
context: { stepCount: result.plan.scriptedPlan.stepSummary.length, envoyId: planningEnvoy.id, delta: result.delta },
|
|
1287
1282
|
});
|
|
1288
1283
|
}
|
|
1289
1284
|
}).catch((err) => {
|
|
@@ -1561,13 +1556,13 @@ export function registerOperationRoutes(
|
|
|
1561
1556
|
}
|
|
1562
1557
|
|
|
1563
1558
|
// Spawn child operation
|
|
1564
|
-
const triggerInput = triggerOp.input as { type: "trigger"; condition: string; responseIntent: string
|
|
1559
|
+
const triggerInput = triggerOp.input as { type: "trigger"; condition: string; responseIntent: string };
|
|
1565
1560
|
const responseType = triggerOp.monitoringDirective?.responseType ?? "maintain";
|
|
1566
1561
|
const childOp = {
|
|
1567
1562
|
id: crypto.randomUUID(),
|
|
1568
1563
|
input: responseType === "deploy"
|
|
1569
1564
|
? { type: "deploy" as const, artifactId: "" }
|
|
1570
|
-
: { type: "maintain" as const, intent: triggerInput.responseIntent
|
|
1565
|
+
: { type: "maintain" as const, intent: triggerInput.responseIntent },
|
|
1571
1566
|
intent: triggerInput.responseIntent,
|
|
1572
1567
|
lineage: triggerOp.id,
|
|
1573
1568
|
triggeredBy: "trigger" as const,
|
|
@@ -1916,9 +1911,9 @@ export function registerOperationRoutes(
|
|
|
1916
1911
|
operationId: childDep.id,
|
|
1917
1912
|
agent: "envoy",
|
|
1918
1913
|
decisionType: "plan-generation",
|
|
1919
|
-
decision: `Child operation plan generated with ${result.plan.
|
|
1914
|
+
decision: `Child operation plan generated with ${result.plan.scriptedPlan.stepSummary.length} steps`,
|
|
1920
1915
|
reasoning: result.plan.reasoning,
|
|
1921
|
-
context: { stepCount: result.plan.
|
|
1916
|
+
context: { stepCount: result.plan.scriptedPlan.stepSummary.length, envoyId: planningEnvoy.id, parentOperationId: parentOp.id },
|
|
1922
1917
|
});
|
|
1923
1918
|
} catch (err) {
|
|
1924
1919
|
const childDep = deployments.get(childId);
|
|
@@ -1952,9 +1947,9 @@ export function registerOperationRoutes(
|
|
|
1952
1947
|
// All children planned — build combined summary plan and await approval
|
|
1953
1948
|
const allChildren = childIds.map((id) => deployments.get(id)).filter(Boolean) as import("@synth-deploy/core").Operation[];
|
|
1954
1949
|
|
|
1955
|
-
const
|
|
1956
|
-
if (!c.plan) return [];
|
|
1957
|
-
return c.plan.
|
|
1950
|
+
const combinedStepSummary = allChildren.flatMap((c, idx) => {
|
|
1951
|
+
if (!c.plan?.scriptedPlan) return [];
|
|
1952
|
+
return c.plan.scriptedPlan.stepSummary.map((step) => ({
|
|
1958
1953
|
...step,
|
|
1959
1954
|
description: `[${idx + 1}/${allChildren.length}: ${c.input.type}] ${step.description}`,
|
|
1960
1955
|
}));
|
|
@@ -1964,10 +1959,35 @@ export function registerOperationRoutes(
|
|
|
1964
1959
|
`Step ${idx + 1} (${c.input.type}): ${c.plan?.reasoning ?? "no reasoning"}`
|
|
1965
1960
|
).join("\n\n");
|
|
1966
1961
|
|
|
1962
|
+
// Combine child execution scripts into a single composite script
|
|
1963
|
+
const combinedScript = allChildren
|
|
1964
|
+
.map((c, idx) => `# --- Child ${idx + 1}/${allChildren.length}: ${c.input.type} ---\n${c.plan?.scriptedPlan?.executionScript ?? "# no script"}`)
|
|
1965
|
+
.join("\n\n");
|
|
1966
|
+
|
|
1967
1967
|
const parentDep = deployments.get(parentOp.id);
|
|
1968
1968
|
if (parentDep && parentDep.status === "pending") {
|
|
1969
|
-
parentDep.plan = {
|
|
1970
|
-
|
|
1969
|
+
parentDep.plan = {
|
|
1970
|
+
scriptedPlan: {
|
|
1971
|
+
platform: "bash",
|
|
1972
|
+
executionScript: combinedScript,
|
|
1973
|
+
dryRunScript: null,
|
|
1974
|
+
rollbackScript: null,
|
|
1975
|
+
reasoning: combinedReasoning,
|
|
1976
|
+
stepSummary: combinedStepSummary,
|
|
1977
|
+
},
|
|
1978
|
+
reasoning: combinedReasoning,
|
|
1979
|
+
};
|
|
1980
|
+
parentDep.rollbackPlan = {
|
|
1981
|
+
scriptedPlan: {
|
|
1982
|
+
platform: "bash",
|
|
1983
|
+
executionScript: "# Child operations handle their own rollback",
|
|
1984
|
+
dryRunScript: null,
|
|
1985
|
+
rollbackScript: null,
|
|
1986
|
+
reasoning: "Child operations handle their own rollback",
|
|
1987
|
+
stepSummary: [],
|
|
1988
|
+
},
|
|
1989
|
+
reasoning: "Child operations handle their own rollback",
|
|
1990
|
+
};
|
|
1971
1991
|
parentDep.status = "awaiting_approval" as typeof parentDep.status;
|
|
1972
1992
|
parentDep.recommendation = computeRecommendation(parentDep, deployments);
|
|
1973
1993
|
deployments.save(parentDep);
|
|
@@ -1979,7 +1999,7 @@ export function registerOperationRoutes(
|
|
|
1979
1999
|
decisionType: "composite-plan-ready",
|
|
1980
2000
|
decision: `All ${allChildren.length} child plans ready — composite awaiting approval`,
|
|
1981
2001
|
reasoning: combinedReasoning,
|
|
1982
|
-
context: { childIds, totalSteps:
|
|
2002
|
+
context: { childIds, totalSteps: combinedStepSummary.length },
|
|
1983
2003
|
});
|
|
1984
2004
|
}
|
|
1985
2005
|
}
|
package/src/api/schemas.ts
CHANGED
|
@@ -245,10 +245,10 @@ export const CreateDeploymentSchema = z.object({
|
|
|
245
245
|
|
|
246
246
|
const ChildOperationInputSchema = z.discriminatedUnion("type", [
|
|
247
247
|
z.object({ type: z.literal("deploy"), artifactId: z.string().min(1), artifactVersionId: z.string().optional() }),
|
|
248
|
-
z.object({ type: z.literal("maintain"), intent: z.string().min(1)
|
|
248
|
+
z.object({ type: z.literal("maintain"), intent: z.string().min(1) }),
|
|
249
249
|
z.object({ type: z.literal("query"), intent: z.string().min(1) }),
|
|
250
250
|
z.object({ type: z.literal("investigate"), intent: z.string().min(1), allowWrite: z.boolean().optional() }),
|
|
251
|
-
z.object({ type: z.literal("trigger"), condition: z.string().min(1), responseIntent: z.string().min(1)
|
|
251
|
+
z.object({ type: z.literal("trigger"), condition: z.string().min(1), responseIntent: z.string().min(1) }),
|
|
252
252
|
]);
|
|
253
253
|
|
|
254
254
|
// --- Operations ---
|
|
@@ -284,37 +284,33 @@ export const RejectDeploymentSchema = z.object({
|
|
|
284
284
|
});
|
|
285
285
|
|
|
286
286
|
export const ModifyDeploymentPlanSchema = z.object({
|
|
287
|
-
|
|
287
|
+
executionScript: z.string().min(1, "Execution script must not be empty"),
|
|
288
|
+
rollbackScript: z.string().optional(),
|
|
289
|
+
reason: z.string().min(1),
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const ScriptedPlanSchema = z.object({
|
|
293
|
+
platform: z.enum(["bash", "powershell"]),
|
|
294
|
+
executionScript: z.string().min(1),
|
|
295
|
+
dryRunScript: z.string().nullable(),
|
|
296
|
+
rollbackScript: z.string().nullable(),
|
|
297
|
+
reasoning: z.string().min(1),
|
|
298
|
+
stepSummary: z.array(z.object({
|
|
288
299
|
description: z.string().min(1),
|
|
289
|
-
action: z.string().min(1),
|
|
290
|
-
target: z.string().min(1),
|
|
291
300
|
reversible: z.boolean(),
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
reason: z.string().min(1),
|
|
301
|
+
})),
|
|
302
|
+
diffFromCurrent: z.array(z.object({ key: z.string(), from: z.string(), to: z.string() })).optional(),
|
|
295
303
|
});
|
|
296
304
|
|
|
297
305
|
export const SubmitPlanSchema = z.object({
|
|
298
306
|
plan: z.object({
|
|
299
|
-
|
|
300
|
-
description: z.string().min(1),
|
|
301
|
-
action: z.string().min(1),
|
|
302
|
-
target: z.string().min(1),
|
|
303
|
-
reversible: z.boolean(),
|
|
304
|
-
rollbackAction: z.string().optional(),
|
|
305
|
-
})).min(1),
|
|
307
|
+
scriptedPlan: ScriptedPlanSchema,
|
|
306
308
|
reasoning: z.string().min(1),
|
|
307
309
|
diffFromCurrent: z.array(z.object({ key: z.string(), from: z.string(), to: z.string() })).optional(),
|
|
308
310
|
diffFromPreviousPlan: z.string().optional(),
|
|
309
311
|
}),
|
|
310
312
|
rollbackPlan: z.object({
|
|
311
|
-
|
|
312
|
-
description: z.string().min(1),
|
|
313
|
-
action: z.string().min(1),
|
|
314
|
-
target: z.string().min(1),
|
|
315
|
-
reversible: z.boolean(),
|
|
316
|
-
rollbackAction: z.string().optional(),
|
|
317
|
-
})),
|
|
313
|
+
scriptedPlan: ScriptedPlanSchema,
|
|
318
314
|
reasoning: z.string().min(1),
|
|
319
315
|
}),
|
|
320
316
|
});
|
|
@@ -98,12 +98,12 @@ export class FleetExecutor {
|
|
|
98
98
|
|
|
99
99
|
const client = this.createEnvoyClient(entry.url, entry.token);
|
|
100
100
|
try {
|
|
101
|
-
const validation = await client.validatePlan(plan.
|
|
101
|
+
const validation = await client.validatePlan(plan.scriptedPlan);
|
|
102
102
|
results.push({
|
|
103
103
|
envoyId,
|
|
104
104
|
envoyName: entry.name,
|
|
105
105
|
validated: validation.valid,
|
|
106
|
-
issues: validation.violations
|
|
106
|
+
issues: validation.violations,
|
|
107
107
|
});
|
|
108
108
|
} catch (err) {
|
|
109
109
|
results.push({
|
|
@@ -246,7 +246,14 @@ export class GraphExecutor {
|
|
|
246
246
|
operationId: node.deploymentId ?? node.id,
|
|
247
247
|
plan: enrichedPlan,
|
|
248
248
|
rollbackPlan: {
|
|
249
|
-
|
|
249
|
+
scriptedPlan: {
|
|
250
|
+
platform: "bash",
|
|
251
|
+
executionScript: "# No rollback plan provided",
|
|
252
|
+
dryRunScript: null,
|
|
253
|
+
rollbackScript: null,
|
|
254
|
+
reasoning: "No rollback plan provided",
|
|
255
|
+
stepSummary: [],
|
|
256
|
+
},
|
|
250
257
|
reasoning: "No rollback plan provided",
|
|
251
258
|
},
|
|
252
259
|
artifactType: "graph-node",
|
|
@@ -419,7 +426,14 @@ export class GraphExecutor {
|
|
|
419
426
|
operationId: node.deploymentId ?? nodeId,
|
|
420
427
|
plan,
|
|
421
428
|
rollbackPlan: {
|
|
422
|
-
|
|
429
|
+
scriptedPlan: {
|
|
430
|
+
platform: "bash",
|
|
431
|
+
executionScript: "# Rollback of rollback not supported",
|
|
432
|
+
dryRunScript: null,
|
|
433
|
+
rollbackScript: null,
|
|
434
|
+
reasoning: "Rollback of rollback not supported",
|
|
435
|
+
stepSummary: [],
|
|
436
|
+
},
|
|
423
437
|
reasoning: "Rollback of rollback not supported",
|
|
424
438
|
},
|
|
425
439
|
artifactType: "graph-node-rollback",
|
package/src/index.ts
CHANGED
|
@@ -296,13 +296,23 @@ if (process.env.SYNTH_SEED_DEMO !== 'false' && partitions.list().length === 0) {
|
|
|
296
296
|
environmentId: prodEnv.id as Deployment["environmentId"], version: "2.3.0", status: "succeeded",
|
|
297
297
|
variables: { ...acmePartition.variables, ...prodEnv.variables },
|
|
298
298
|
plan: {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
299
|
+
scriptedPlan: {
|
|
300
|
+
platform: "bash",
|
|
301
|
+
executionScript: "#!/usr/bin/env bash\nset -euo pipefail\nsystemctl stop web-app\ncp -r /opt/web-app/ /opt/web-app.bak/\ntar -xzf /opt/releases/web-app-2.3.0.tar.gz -C /opt/web-app/\nenvsubst < /opt/web-app/config.template > /opt/web-app/.env\nsystemctl start web-app\ncurl -f --retry 3 --retry-delay 5 http://localhost:8080/health",
|
|
302
|
+
dryRunScript: null,
|
|
303
|
+
rollbackScript: "#!/usr/bin/env bash\nset -euo pipefail\nsystemctl stop web-app\ncp -r /opt/web-app.bak/ /opt/web-app/\ncp /opt/web-app.bak/.env /opt/web-app/.env\nsystemctl start web-app",
|
|
304
|
+
reasoning: "Standard 5-step deploy: stop, backup, extract, config, start. One config change: API_ENDPOINT updated to v2 endpoint validated in staging for 4h.",
|
|
305
|
+
stepSummary: [
|
|
306
|
+
{ description: "Stop service", reversible: true },
|
|
307
|
+
{ description: "Backup current binaries", reversible: false },
|
|
308
|
+
{ description: "Deploy new artifact", reversible: true },
|
|
309
|
+
{ description: "Apply environment config (1 variable changed: API_ENDPOINT)", reversible: true },
|
|
310
|
+
{ description: "Start service and verify health endpoint → 200 OK", reversible: true },
|
|
311
|
+
],
|
|
312
|
+
diffFromCurrent: [
|
|
313
|
+
{ key: "API_ENDPOINT", from: "https://api.acme.corp/v1", to: "https://api.acme.corp/v2" },
|
|
314
|
+
],
|
|
315
|
+
},
|
|
306
316
|
reasoning: "Standard 5-step deploy: stop, backup, extract, config, start. One config change: API_ENDPOINT updated to v2 endpoint validated in staging for 4h.",
|
|
307
317
|
diffFromCurrent: [
|
|
308
318
|
{ key: "API_ENDPOINT", from: "https://api.acme.corp/v1", to: "https://api.acme.corp/v2" },
|
|
@@ -367,12 +377,19 @@ if (process.env.SYNTH_SEED_DEMO !== 'false' && partitions.list().length === 0) {
|
|
|
367
377
|
environmentId: stagingEnv.id as Deployment["environmentId"], version: "1.13.0-beta.2", status: "running",
|
|
368
378
|
variables: { ...globexPartition.variables, ...stagingEnv.variables },
|
|
369
379
|
plan: {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
380
|
+
scriptedPlan: {
|
|
381
|
+
platform: "bash",
|
|
382
|
+
executionScript: "#!/usr/bin/env bash\nset -euo pipefail\ndocker pull registry.internal/api:1.13.0-beta.2\ndocker stop api-staging\ndocker run -d --name api-staging --env-file /opt/api/.env -p 8080:8080 registry.internal/api:1.13.0-beta.2\ncurl -f --retry 3 --retry-delay 5 http://localhost:8080/health",
|
|
383
|
+
dryRunScript: null,
|
|
384
|
+
rollbackScript: "#!/usr/bin/env bash\nset -euo pipefail\ndocker stop api-staging\ndocker pull registry.internal/api:1.12.0\ndocker start api-staging",
|
|
385
|
+
reasoning: "Container swap: pull new image, stop old container, start new one, verify health. Staging environment — rollback is fast via image tag swap.",
|
|
386
|
+
stepSummary: [
|
|
387
|
+
{ description: "Pull latest image from registry", reversible: true },
|
|
388
|
+
{ description: "Stop running container", reversible: true },
|
|
389
|
+
{ description: "Start new container with updated image", reversible: true },
|
|
390
|
+
{ description: "Verify health endpoint returns 200", reversible: false },
|
|
391
|
+
],
|
|
392
|
+
},
|
|
376
393
|
reasoning: "Container swap: pull new image, stop old container, start new one, verify health. Staging environment — rollback is fast via image tag swap.",
|
|
377
394
|
},
|
|
378
395
|
debriefEntryIds: [],
|
|
@@ -383,14 +400,21 @@ if (process.env.SYNTH_SEED_DEMO !== 'false' && partitions.list().length === 0) {
|
|
|
383
400
|
environmentId: prodEnv.id as Deployment["environmentId"], version: "3.1.0", status: "awaiting_approval",
|
|
384
401
|
variables: { ...globexPartition.variables, ...prodEnv.variables },
|
|
385
402
|
plan: {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
403
|
+
scriptedPlan: {
|
|
404
|
+
platform: "bash",
|
|
405
|
+
executionScript: "#!/usr/bin/env bash\nset -euo pipefail\nnpm run worker:drain --timeout=120\nsystemctl stop synth-worker\ncp -r /opt/releases/worker-3.1.0/* /opt/worker/\nenvsubst < /opt/worker/config.template > /opt/worker/.env\nsystemctl start synth-worker\ncurl -f --retry 6 --retry-delay 5 http://localhost:9090/metrics",
|
|
406
|
+
dryRunScript: null,
|
|
407
|
+
rollbackScript: "#!/usr/bin/env bash\nset -euo pipefail\nsystemctl stop synth-worker\ncp -r /opt/worker.bak/* /opt/worker/\ncp /opt/worker.bak/.env /opt/worker/.env\nsystemctl start synth-worker",
|
|
408
|
+
reasoning: "Worker upgrade with concurrency increase. Drain first to avoid job loss, then replace binary and config atomically. Queue depth check confirms processing resumed.",
|
|
409
|
+
stepSummary: [
|
|
410
|
+
{ description: "Drain queue — wait for in-flight jobs to complete", reversible: false },
|
|
411
|
+
{ description: "Stop worker processes on all nodes", reversible: true },
|
|
412
|
+
{ description: "Deploy new worker binary", reversible: true },
|
|
413
|
+
{ description: "Update queue concurrency config (WORKER_CONCURRENCY: 4 → 8)", reversible: true },
|
|
414
|
+
{ description: "Start worker and verify queue depth drops", reversible: true },
|
|
415
|
+
{ description: "Verify queue processing resumes within 30s", reversible: false },
|
|
416
|
+
],
|
|
417
|
+
},
|
|
394
418
|
reasoning: "Worker upgrade with concurrency increase. Drain first to avoid job loss, then replace binary and config atomically. Queue depth check confirms processing resumed.",
|
|
395
419
|
},
|
|
396
420
|
debriefEntryIds: [],
|
|
@@ -24,8 +24,8 @@ vi.mock("../src/agent/envoy-client.js", () => ({
|
|
|
24
24
|
EnvoyClient: vi.fn().mockImplementation(function (this: any) {
|
|
25
25
|
this.requestPlan = vi.fn().mockResolvedValue({
|
|
26
26
|
blocked: false,
|
|
27
|
-
plan: {
|
|
28
|
-
rollbackPlan: {
|
|
27
|
+
plan: { scriptedPlan: { platform: "bash", executionScript: "echo test", dryRunScript: null, rollbackScript: null, reasoning: "test plan", stepSummary: [{ description: "Step 1", reversible: false }] }, reasoning: "test plan" },
|
|
28
|
+
rollbackPlan: { scriptedPlan: { platform: "bash", executionScript: "echo rollback", dryRunScript: null, rollbackScript: null, reasoning: "no rollback", stepSummary: [] }, reasoning: "no rollback" },
|
|
29
29
|
});
|
|
30
30
|
this.executeApprovedPlan = vi.fn().mockResolvedValue({});
|
|
31
31
|
this.removeMonitoringDirective = vi.fn().mockResolvedValue({});
|
|
@@ -63,7 +63,7 @@ const MOCK_REGISTRY: EnvoyRegistry = {
|
|
|
63
63
|
} as unknown as EnvoyRegistry;
|
|
64
64
|
|
|
65
65
|
const MOCK_PLAN: OperationPlan = {
|
|
66
|
-
|
|
66
|
+
scriptedPlan: { platform: "bash", executionScript: "systemctl status", dryRunScript: null, rollbackScript: null, reasoning: "standard maintenance check", stepSummary: [{ description: "Check services", reversible: false }] },
|
|
67
67
|
reasoning: "standard maintenance check",
|
|
68
68
|
};
|
|
69
69
|
|
|
@@ -284,8 +284,8 @@ describe("Composite Operations — planning via envoy registry", () => {
|
|
|
284
284
|
this.requestPlan = vi.fn().mockResolvedValue({
|
|
285
285
|
blocked: true,
|
|
286
286
|
blockReason: "insufficient permissions",
|
|
287
|
-
plan: {
|
|
288
|
-
rollbackPlan: {
|
|
287
|
+
plan: { scriptedPlan: { platform: "bash", executionScript: "", dryRunScript: null, rollbackScript: null, reasoning: "", stepSummary: [] }, reasoning: "" },
|
|
288
|
+
rollbackPlan: { scriptedPlan: { platform: "bash", executionScript: "", dryRunScript: null, rollbackScript: null, reasoning: "", stepSummary: [] }, reasoning: "" },
|
|
289
289
|
});
|
|
290
290
|
});
|
|
291
291
|
|
|
@@ -409,7 +409,7 @@ describe("Composite Operations — approval and execution", () => {
|
|
|
409
409
|
// Child has a plan but no envoyId, and this ctx has no envoy registry
|
|
410
410
|
seedChild(ctx.deployments, parent.id, {
|
|
411
411
|
plan: MOCK_PLAN,
|
|
412
|
-
rollbackPlan: {
|
|
412
|
+
rollbackPlan: { scriptedPlan: { platform: "bash", executionScript: "", dryRunScript: null, rollbackScript: null, reasoning: "", stepSummary: [] }, reasoning: "" },
|
|
413
413
|
});
|
|
414
414
|
|
|
415
415
|
await ctx.app.inject({
|