fix(ql3): seal ordered reconciliation evidence

This commit is contained in:
whyour
2026-09-02 08:45:43 +08:00
parent cbdddffc91
commit 83966a1287
2 changed files with 71 additions and 3 deletions
@@ -86,6 +86,7 @@ interface AutomationProof {
interface SecretConfigProof {
readonly intent: Readonly<LocalReconciliationSecretConfigApplyIntent>;
readonly receipt: Readonly<LocalReconciliationSecretConfigApplyReceipt>;
readonly preparedHeadDigest: string;
readonly paths: ReturnType<typeof localReconciliationSecretConfigApplyPaths>;
readonly storageState: 'applied' | 'completed';
}
@@ -107,6 +108,7 @@ export interface LocalReconciliationCompletionDependencies
async function runHistoryProof(
command: Readonly<LocalReconciliationCompleteCommand>,
terminal: Readonly<LocalReconciliationApplicationTerminal>,
secretConfig: Readonly<SecretConfigProof> | null,
uid: number,
dependencies: LocalReconciliationCompletionDependencies,
): Promise<Readonly<RunHistoryProof> | null> {
@@ -153,7 +155,8 @@ async function runHistoryProof(
command.request.runHistory.expectedPreservationDigest ||
history.receipt.applicationPlanDigest !==
terminal.plan.applicationPlanDigest ||
history.receipt.sourceHeadDigest !== command.request.expectedHeadDigest
history.receipt.sourceHeadDigest !==
(secretConfig?.preparedHeadDigest ?? command.request.expectedHeadDigest)
) {
fail('run history preservation evidence is detached');
}
@@ -571,6 +574,7 @@ async function secretConfigProof(
selected,
uid,
);
const targetSnapshotSha256 = decision.context.planHeader.targetSnapshotSha256;
if (
intent.command.options.deploymentRoot !== command.options.deploymentRoot ||
intent.command.options.applicationRoot !==
@@ -589,8 +593,9 @@ async function secretConfigProof(
receipt.decisionId !== binding.decisionId ||
receipt.applyDigest !== binding.expectedApplyDigest ||
receipt.preparationDigest !== intent.preparationDigest ||
(automation !== null &&
intent.backup.sha256 !== automation.receipt.targetAfter.sha256) ||
(automation === null
? targetSnapshotSha256 !== null
: targetSnapshotSha256 !== automation.receipt.targetAfter.sha256) ||
fs.existsSync(selected.rollbackReceipt)
) {
fail('secret config apply evidence is detached');
@@ -623,6 +628,7 @@ async function secretConfigProof(
return Object.freeze({
intent,
receipt,
preparedHeadDigest: decision.context.planHeader.preparedHeadDigest,
paths: selected,
storageState,
});
@@ -820,6 +826,7 @@ export async function completeLocalReconciliation(
const runHistory = await runHistoryProof(
command,
terminal,
secretConfig,
uid,
dependencies,
);
@@ -1015,6 +1022,7 @@ export async function verifyLocalReconciliationCompletion(
const runHistory = await runHistoryProof(
syntheticCompleteCommand,
terminal,
secretConfig,
uid,
dependencies,
);
@@ -4402,6 +4402,66 @@ test('Secret/Config plan follows applied Automation and preserved Run History on
);
assert.equal(applied.state, 'reconciliation_secret_config_applied');
assert.equal(applied.activeBindingCount, 1);
const completionRoot = path.join(
path.dirname(state.captureRoot),
'cross-domain-completion',
);
fs.mkdirSync(completionRoot, { mode: 0o700 });
const completionCommand = {
schemaVersion: 3,
operation: 'local.deployment.reconciliation.complete',
options: {
deploymentRoot: state.deploymentRoot,
applicationRoot: state.applicationRoot,
completionRoot,
automation: {
automationRoot: state.automationRoot,
automationDecisionRoot: state.automationDecisionRoot,
automationApplyRoot: state.automationApplyRoot,
targetDatabasePath: state.targetDatabasePath,
},
secretConfig: {
secretConfigRoot,
secretConfigDecisionRoot,
secretConfigApplyRoot,
targetDatabasePath: state.targetDatabasePath,
},
runHistory: {
runHistoryRoot,
decisionFilePath: state.reviewFile.filePath,
},
allowRootService: rootAcknowledgement(),
},
request: {
completionId: '00000000-0000-4000-8000-000000000434',
applicationId: state.application.applicationId,
expectedApplicationPlanDigest: state.application.applicationPlanDigest,
expectedHeadDigest: applied.instanceHeadDigest,
automation: {
automationId: state.automationCommand.request.automationId,
decisionId: state.decisionId,
expectedApplyDigest: state.applied.applyDigest,
},
secretConfig: {
secretConfigId,
decisionId: secretConfigDecisionId,
expectedApplyDigest: applied.applyDigest,
},
runHistory: {
preservationId: preservationCommand.request.preservationId,
expectedPreservationDigest: preserved.preservationDigest,
},
completedAtMs: appliedAtMs + 1,
},
};
const completed = await completeLocalReconciliation(completionCommand);
assert.equal(completed.state, 'reconciliation_completed');
assert.equal(completed.adapterCount, 3);
assert.equal(
(await completeLocalReconciliation(completionCommand)).status,
'existing',
);
});
test('Secret/Config decision reauthenticates the same reviewer, seals exact candidates and verifies content-free', async (t) => {