mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-09 18:32:52 +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:
@@ -37,6 +37,8 @@ export interface ConfirmedActionOutcome {
|
||||
|
||||
export interface ConfirmedPlanResult {
|
||||
outcomes: ConfirmedActionOutcome[];
|
||||
/** True when the approval gate refused execution (Apply must not claim success). */
|
||||
refused?: boolean;
|
||||
}
|
||||
|
||||
export interface ConfirmedOutcomeSummary {
|
||||
@@ -78,6 +80,17 @@ export function messageForConfirmedOutcomes(summary: ConfirmedOutcomeSummary): s
|
||||
return 'Rollout confirmed';
|
||||
}
|
||||
|
||||
/** Apply finished a confirmed snapshot, but live approval is no longer current. */
|
||||
export function messageForSnapshotFinishedWithStaleApproval(summary: ConfirmedOutcomeSummary): string {
|
||||
if (summary.failed > 0) {
|
||||
return 'Confirmed snapshot finished with node failures; approval is no longer current';
|
||||
}
|
||||
if (summary.pending > 0) {
|
||||
return 'Confirmed snapshot finished with actions still in progress; approval is no longer current';
|
||||
}
|
||||
return 'Confirmed snapshot finished; approval is no longer current';
|
||||
}
|
||||
|
||||
function mapDeployOutcome(
|
||||
base: { nodeId: number; nodeName: string; action: PreviewAction },
|
||||
result: DeployOutcome,
|
||||
@@ -195,12 +208,18 @@ export class BlueprintReconciler {
|
||||
): Promise<ConfirmedPlanResult> {
|
||||
const blueprint = DatabaseService.getInstance().getBlueprint(blueprintId);
|
||||
if (!blueprint || !blueprint.enabled) {
|
||||
return { outcomes: [] };
|
||||
return { outcomes: [], refused: true };
|
||||
}
|
||||
const parsed = parseApprovedBlastJson(blueprint.approved_blast_json);
|
||||
if (!parsed.ok || blueprint.approval_status !== 'approved') {
|
||||
diagnosticLog('reconcileConfirmedPlan skipped: approval missing or invalid', { blueprintId });
|
||||
return { outcomes: [] };
|
||||
// Same fail-closed gate as tick reconcile: never execute when approval is
|
||||
// missing, invalid, or the stored fingerprint no longer matches live intent.
|
||||
if (
|
||||
!parsed.ok
|
||||
|| blueprint.approval_status !== 'approved'
|
||||
|| blueprint.approved_intent_fingerprint !== intentFingerprint(blueprint)
|
||||
) {
|
||||
diagnosticLog('reconcileConfirmedPlan skipped: approval missing, invalid, or drifted', { blueprintId });
|
||||
return { outcomes: [], refused: true };
|
||||
}
|
||||
const authorized = filterAuthorizedExecutorActions(parsed.entries, executorActions);
|
||||
const nodes = DatabaseService.getInstance().getNodes();
|
||||
|
||||
Reference in New Issue
Block a user