Skip to content

Commit 83966a1

Browse files
committed
fix(ql3): seal ordered reconciliation evidence
1 parent cbdddff commit 83966a1

2 files changed

Lines changed: 71 additions & 3 deletions

File tree

packages/ql3-local-owner-cli/src/deployment/reconciliation/completion/coordinator.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ interface AutomationProof {
8686
interface SecretConfigProof {
8787
readonly intent: Readonly<LocalReconciliationSecretConfigApplyIntent>;
8888
readonly receipt: Readonly<LocalReconciliationSecretConfigApplyReceipt>;
89+
readonly preparedHeadDigest: string;
8990
readonly paths: ReturnType<typeof localReconciliationSecretConfigApplyPaths>;
9091
readonly storageState: 'applied' | 'completed';
9192
}
@@ -107,6 +108,7 @@ export interface LocalReconciliationCompletionDependencies
107108
async function runHistoryProof(
108109
command: Readonly<LocalReconciliationCompleteCommand>,
109110
terminal: Readonly<LocalReconciliationApplicationTerminal>,
111+
secretConfig: Readonly<SecretConfigProof> | null,
110112
uid: number,
111113
dependencies: LocalReconciliationCompletionDependencies,
112114
): Promise<Readonly<RunHistoryProof> | null> {
@@ -153,7 +155,8 @@ async function runHistoryProof(
153155
command.request.runHistory.expectedPreservationDigest ||
154156
history.receipt.applicationPlanDigest !==
155157
terminal.plan.applicationPlanDigest ||
156-
history.receipt.sourceHeadDigest !== command.request.expectedHeadDigest
158+
history.receipt.sourceHeadDigest !==
159+
(secretConfig?.preparedHeadDigest ?? command.request.expectedHeadDigest)
157160
) {
158161
fail('run history preservation evidence is detached');
159162
}
@@ -571,6 +574,7 @@ async function secretConfigProof(
571574
selected,
572575
uid,
573576
);
577+
const targetSnapshotSha256 = decision.context.planHeader.targetSnapshotSha256;
574578
if (
575579
intent.command.options.deploymentRoot !== command.options.deploymentRoot ||
576580
intent.command.options.applicationRoot !==
@@ -589,8 +593,9 @@ async function secretConfigProof(
589593
receipt.decisionId !== binding.decisionId ||
590594
receipt.applyDigest !== binding.expectedApplyDigest ||
591595
receipt.preparationDigest !== intent.preparationDigest ||
592-
(automation !== null &&
593-
intent.backup.sha256 !== automation.receipt.targetAfter.sha256) ||
596+
(automation === null
597+
? targetSnapshotSha256 !== null
598+
: targetSnapshotSha256 !== automation.receipt.targetAfter.sha256) ||
594599
fs.existsSync(selected.rollbackReceipt)
595600
) {
596601
fail('secret config apply evidence is detached');
@@ -623,6 +628,7 @@ async function secretConfigProof(
623628
return Object.freeze({
624629
intent,
625630
receipt,
631+
preparedHeadDigest: decision.context.planHeader.preparedHeadDigest,
626632
paths: selected,
627633
storageState,
628634
});
@@ -820,6 +826,7 @@ export async function completeLocalReconciliation(
820826
const runHistory = await runHistoryProof(
821827
command,
822828
terminal,
829+
secretConfig,
823830
uid,
824831
dependencies,
825832
);
@@ -1015,6 +1022,7 @@ export async function verifyLocalReconciliationCompletion(
10151022
const runHistory = await runHistoryProof(
10161023
syntheticCompleteCommand,
10171024
terminal,
1025+
secretConfig,
10181026
uid,
10191027
dependencies,
10201028
);

packages/ql3-local-owner-cli/test/reconciliationCapturePrepare.test.cjs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4402,6 +4402,66 @@ test('Secret/Config plan follows applied Automation and preserved Run History on
44024402
);
44034403
assert.equal(applied.state, 'reconciliation_secret_config_applied');
44044404
assert.equal(applied.activeBindingCount, 1);
4405+
4406+
const completionRoot = path.join(
4407+
path.dirname(state.captureRoot),
4408+
'cross-domain-completion',
4409+
);
4410+
fs.mkdirSync(completionRoot, { mode: 0o700 });
4411+
const completionCommand = {
4412+
schemaVersion: 3,
4413+
operation: 'local.deployment.reconciliation.complete',
4414+
options: {
4415+
deploymentRoot: state.deploymentRoot,
4416+
applicationRoot: state.applicationRoot,
4417+
completionRoot,
4418+
automation: {
4419+
automationRoot: state.automationRoot,
4420+
automationDecisionRoot: state.automationDecisionRoot,
4421+
automationApplyRoot: state.automationApplyRoot,
4422+
targetDatabasePath: state.targetDatabasePath,
4423+
},
4424+
secretConfig: {
4425+
secretConfigRoot,
4426+
secretConfigDecisionRoot,
4427+
secretConfigApplyRoot,
4428+
targetDatabasePath: state.targetDatabasePath,
4429+
},
4430+
runHistory: {
4431+
runHistoryRoot,
4432+
decisionFilePath: state.reviewFile.filePath,
4433+
},
4434+
allowRootService: rootAcknowledgement(),
4435+
},
4436+
request: {
4437+
completionId: '00000000-0000-4000-8000-000000000434',
4438+
applicationId: state.application.applicationId,
4439+
expectedApplicationPlanDigest: state.application.applicationPlanDigest,
4440+
expectedHeadDigest: applied.instanceHeadDigest,
4441+
automation: {
4442+
automationId: state.automationCommand.request.automationId,
4443+
decisionId: state.decisionId,
4444+
expectedApplyDigest: state.applied.applyDigest,
4445+
},
4446+
secretConfig: {
4447+
secretConfigId,
4448+
decisionId: secretConfigDecisionId,
4449+
expectedApplyDigest: applied.applyDigest,
4450+
},
4451+
runHistory: {
4452+
preservationId: preservationCommand.request.preservationId,
4453+
expectedPreservationDigest: preserved.preservationDigest,
4454+
},
4455+
completedAtMs: appliedAtMs + 1,
4456+
},
4457+
};
4458+
const completed = await completeLocalReconciliation(completionCommand);
4459+
assert.equal(completed.state, 'reconciliation_completed');
4460+
assert.equal(completed.adapterCount, 3);
4461+
assert.equal(
4462+
(await completeLocalReconciliation(completionCommand)).status,
4463+
'existing',
4464+
);
44054465
});
44064466

44074467
test('Secret/Config decision reauthenticates the same reviewer, seals exact candidates and verifies content-free', async (t) => {

0 commit comments

Comments
 (0)