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 { interface SecretConfigProof {
readonly intent: Readonly<LocalReconciliationSecretConfigApplyIntent>; readonly intent: Readonly<LocalReconciliationSecretConfigApplyIntent>;
readonly receipt: Readonly<LocalReconciliationSecretConfigApplyReceipt>; readonly receipt: Readonly<LocalReconciliationSecretConfigApplyReceipt>;
readonly preparedHeadDigest: string;
readonly paths: ReturnType<typeof localReconciliationSecretConfigApplyPaths>; readonly paths: ReturnType<typeof localReconciliationSecretConfigApplyPaths>;
readonly storageState: 'applied' | 'completed'; readonly storageState: 'applied' | 'completed';
} }
@@ -107,6 +108,7 @@ export interface LocalReconciliationCompletionDependencies
async function runHistoryProof( async function runHistoryProof(
command: Readonly<LocalReconciliationCompleteCommand>, command: Readonly<LocalReconciliationCompleteCommand>,
terminal: Readonly<LocalReconciliationApplicationTerminal>, terminal: Readonly<LocalReconciliationApplicationTerminal>,
secretConfig: Readonly<SecretConfigProof> | null,
uid: number, uid: number,
dependencies: LocalReconciliationCompletionDependencies, dependencies: LocalReconciliationCompletionDependencies,
): Promise<Readonly<RunHistoryProof> | null> { ): Promise<Readonly<RunHistoryProof> | null> {
@@ -153,7 +155,8 @@ async function runHistoryProof(
command.request.runHistory.expectedPreservationDigest || command.request.runHistory.expectedPreservationDigest ||
history.receipt.applicationPlanDigest !== history.receipt.applicationPlanDigest !==
terminal.plan.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'); fail('run history preservation evidence is detached');
} }
@@ -571,6 +574,7 @@ async function secretConfigProof(
selected, selected,
uid, uid,
); );
const targetSnapshotSha256 = decision.context.planHeader.targetSnapshotSha256;
if ( if (
intent.command.options.deploymentRoot !== command.options.deploymentRoot || intent.command.options.deploymentRoot !== command.options.deploymentRoot ||
intent.command.options.applicationRoot !== intent.command.options.applicationRoot !==
@@ -589,8 +593,9 @@ async function secretConfigProof(
receipt.decisionId !== binding.decisionId || receipt.decisionId !== binding.decisionId ||
receipt.applyDigest !== binding.expectedApplyDigest || receipt.applyDigest !== binding.expectedApplyDigest ||
receipt.preparationDigest !== intent.preparationDigest || receipt.preparationDigest !== intent.preparationDigest ||
(automation !== null && (automation === null
intent.backup.sha256 !== automation.receipt.targetAfter.sha256) || ? targetSnapshotSha256 !== null
: targetSnapshotSha256 !== automation.receipt.targetAfter.sha256) ||
fs.existsSync(selected.rollbackReceipt) fs.existsSync(selected.rollbackReceipt)
) { ) {
fail('secret config apply evidence is detached'); fail('secret config apply evidence is detached');
@@ -623,6 +628,7 @@ async function secretConfigProof(
return Object.freeze({ return Object.freeze({
intent, intent,
receipt, receipt,
preparedHeadDigest: decision.context.planHeader.preparedHeadDigest,
paths: selected, paths: selected,
storageState, storageState,
}); });
@@ -820,6 +826,7 @@ export async function completeLocalReconciliation(
const runHistory = await runHistoryProof( const runHistory = await runHistoryProof(
command, command,
terminal, terminal,
secretConfig,
uid, uid,
dependencies, dependencies,
); );
@@ -1015,6 +1022,7 @@ export async function verifyLocalReconciliationCompletion(
const runHistory = await runHistoryProof( const runHistory = await runHistoryProof(
syntheticCompleteCommand, syntheticCompleteCommand,
terminal, terminal,
secretConfig,
uid, uid,
dependencies, 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.state, 'reconciliation_secret_config_applied');
assert.equal(applied.activeBindingCount, 1); 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) => { test('Secret/Config decision reauthenticates the same reviewer, seals exact candidates and verifies content-free', async (t) => {