mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-25 09:46:47 +00:00
fix(blueprints): gate confirmed apply on live intent fingerprint (#1663)
* fix(blueprints): gate confirmed apply on live intent fingerprint reconcileConfirmedPlan only checked approval_status, so a concurrent compose edit during Apply could deploy unapproved content under a stale fingerprint. Match the tick-path fingerprint gate, refuse Apply when live intent drifts, and surface reconciler refusal as PREVIEW_STALE instead of a false success. * test(blueprints): cover matching-fingerprint reconcileConfirmedPlan path Prove the production allow branch still deploys authorized actions when the approval fingerprint matches, and does not execute unauthorized blast nodes. * fix(blueprints): report live approval after confirmed snapshot apply A concurrent edit can clear approval while multi-node snapshot deploy is still running. Keep the in-flight snapshot contract, but re-read live effectiveApproval before responding and warn in the rollout dialog when approval is no longer current.
This commit is contained in:
@@ -11,6 +11,7 @@ import { BlueprintService } from '../services/BlueprintService';
|
||||
import {
|
||||
BlueprintReconciler,
|
||||
messageForConfirmedOutcomes,
|
||||
messageForSnapshotFinishedWithStaleApproval,
|
||||
summarizeConfirmedOutcomes,
|
||||
} from '../services/BlueprintReconciler';
|
||||
import { BlueprintAnalyzer } from '../services/BlueprintAnalyzer';
|
||||
@@ -18,6 +19,8 @@ import { buildBlueprintPreview, evaluateLightweightEffectiveApproval } from '../
|
||||
import {
|
||||
confirmableActionsEqual,
|
||||
deriveBlastFromConfirmableActions,
|
||||
evaluateEffectiveApproval,
|
||||
intentFingerprint,
|
||||
parseConfirmableActionsBody,
|
||||
serializeApprovedBlast,
|
||||
} from '../services/blueprintApproval';
|
||||
@@ -427,17 +430,42 @@ blueprintsRouter.post('/:id/apply', async (req: Request, res: Response): Promise
|
||||
}
|
||||
|
||||
const blast = deriveBlastFromConfirmableActions(preview.confirmableActions);
|
||||
DatabaseService.getInstance().setBlueprintApproval(id, {
|
||||
const approved = DatabaseService.getInstance().setBlueprintApproval(id, {
|
||||
intentFingerprint: preview.planFingerprint,
|
||||
blastJson: serializeApprovedBlast(blast),
|
||||
approvedBy: req.user?.username ?? null,
|
||||
});
|
||||
const plan = await BlueprintReconciler.getInstance().reconcileConfirmedPlan(id, preview.executorActions);
|
||||
// planFingerprint is from an earlier preview. Concurrent compose/selector
|
||||
// edits can invalidate it before approval persists, or the reconciler can
|
||||
// refuse if the live gate no longer matches. Both paths clear and 409.
|
||||
const approvalMatches = !!approved && intentFingerprint(approved) === preview.planFingerprint;
|
||||
const plan = approvalMatches
|
||||
? await BlueprintReconciler.getInstance().reconcileConfirmedPlan(id, preview.executorActions)
|
||||
: null;
|
||||
if (!plan || plan.refused) {
|
||||
DatabaseService.getInstance().clearBlueprintApproval(id);
|
||||
const fresh = await buildBlueprintPreview(id);
|
||||
res.status(409).json({
|
||||
error: 'Preview is stale; refresh and confirm again',
|
||||
code: 'PREVIEW_STALE',
|
||||
preview: fresh,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Snapshot deploy may finish after a concurrent edit cleared approval.
|
||||
// Report live effectiveApproval; do not hardcode "approved".
|
||||
const live = DatabaseService.getInstance().getBlueprint(id);
|
||||
const { effectiveApproval } = live
|
||||
? evaluateEffectiveApproval(live, preview.executorActions)
|
||||
: { effectiveApproval: 'pending' as const };
|
||||
const outcomeSummary = summarizeConfirmedOutcomes(plan.outcomes);
|
||||
const message = effectiveApproval === 'approved'
|
||||
? messageForConfirmedOutcomes(outcomeSummary)
|
||||
: messageForSnapshotFinishedWithStaleApproval(outcomeSummary);
|
||||
res.json({
|
||||
message: messageForConfirmedOutcomes(outcomeSummary),
|
||||
message,
|
||||
blueprintId: id,
|
||||
effectiveApproval: 'approved',
|
||||
effectiveApproval,
|
||||
outcomes: plan.outcomes,
|
||||
outcomeSummary,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user