feat(ql3): add secret config completion evidence

This commit is contained in:
whyour
2026-08-24 11:54:25 +08:00
parent 8ad8ed96bd
commit d94ca02e0d
10 changed files with 1093 additions and 55 deletions
@@ -501,7 +501,8 @@ export function advanceLocalCutoverInstanceHead(
current.state === 'reconciliation_secret_config_applied') ||
(state === 'reconciliation_completed' &&
(current.state === 'reconciliation_application_planned' ||
current.state === 'reconciliation_automation_applied')) ||
current.state === 'reconciliation_automation_applied' ||
current.state === 'reconciliation_secret_config_applied')) ||
(state === 'rollback_prepared' && current.state === 'target_stopped') ||
(state === 'legacy_restart_requested' &&
current.state === 'rollback_prepared') ||
@@ -570,6 +570,78 @@ export function validateLocalReconciliationSecretConfigAppliedStorage(
validateBackup(selected, intent, uid, [0o400]);
}
/**
* Collects the rollback copy only after the cross-domain completion head is
* durable. Encrypted material and receipts remain sealed as audit evidence.
* The partially collected layout is accepted so a response loss can replay.
*/
export function collectLocalReconciliationSecretConfigCompletedStorage(
selected: Readonly<LocalReconciliationSecretConfigApplyPaths>,
intent: Readonly<LocalReconciliationSecretConfigApplyIntent>,
uid: number,
): void {
directoryMode(selected.root, uid, [0o500], 'root');
directoryMode(selected.backupRoot, uid, [0o700, 0o500], 'backup root');
directoryMode(
selected.rollbackRoot,
uid,
[0o700, 0o500],
'rollback work root',
);
validateLocalReconciliationSecretConfigApplyCatalog(selected);
if (fs.readdirSync(selected.rollbackRoot).length !== 0) {
fail('rollback work root must be empty');
}
readLocalReconciliationSecretConfigApplyIntent(selected, uid);
readLocalReconciliationSecretConfigApplyReceipt(selected, uid);
readLocalReconciliationSecretConfigMaterials(
selected,
intent.profile,
uid,
intent.material,
);
if (fs.existsSync(selected.backup)) {
validateBackup(selected, intent, uid, [0o400]);
if ((fs.statSync(selected.backupRoot).mode & 0o777) !== 0o700) {
fs.chmodSync(selected.backupRoot, 0o700);
syncDirectory(selected.root);
}
unlinkIfPresent(selected.backup);
}
sealDirectory(selected.backupRoot, uid, 'backup root');
sealDirectory(selected.rollbackRoot, uid, 'rollback work root');
validateLocalReconciliationSecretConfigCompletedStorage(
selected,
intent,
uid,
);
}
export function validateLocalReconciliationSecretConfigCompletedStorage(
selected: Readonly<LocalReconciliationSecretConfigApplyPaths>,
intent: Readonly<LocalReconciliationSecretConfigApplyIntent>,
uid: number,
): void {
directoryMode(selected.root, uid, [0o500], 'root');
directoryMode(selected.backupRoot, uid, [0o500], 'backup root');
directoryMode(selected.rollbackRoot, uid, [0o500], 'rollback work root');
validateLocalReconciliationSecretConfigApplyCatalog(selected);
if (fs.readdirSync(selected.backupRoot).length !== 0) {
fail('backup root must be empty');
}
if (fs.readdirSync(selected.rollbackRoot).length !== 0) {
fail('rollback work root must be empty');
}
readLocalReconciliationSecretConfigApplyIntent(selected, uid);
readLocalReconciliationSecretConfigApplyReceipt(selected, uid);
readLocalReconciliationSecretConfigMaterials(
selected,
intent.profile,
uid,
intent.material,
);
}
export function prepareLocalReconciliationSecretConfigRollbackSource(
selected: Readonly<LocalReconciliationSecretConfigApplyPaths>,
intent: Readonly<LocalReconciliationSecretConfigApplyIntent>,
@@ -17,11 +17,19 @@ export interface LocalReconciliationCompletionAutomationOptions {
readonly targetDatabasePath: string;
}
export interface LocalReconciliationCompletionSecretConfigOptions {
readonly secretConfigRoot: string;
readonly secretConfigDecisionRoot: string;
readonly secretConfigApplyRoot: string;
readonly targetDatabasePath: string;
}
export interface LocalReconciliationCompletionOptions {
readonly deploymentRoot: string;
readonly applicationRoot: string;
readonly completionRoot: string;
readonly automation: Readonly<LocalReconciliationCompletionAutomationOptions> | null;
readonly secretConfig: Readonly<LocalReconciliationCompletionSecretConfigOptions> | null;
readonly runHistory: Readonly<LocalReconciliationCompletionRunHistoryOptions> | null;
readonly allowRootService: boolean;
}
@@ -37,13 +45,19 @@ export interface LocalReconciliationCompletionAutomationBinding {
readonly expectedApplyDigest: string;
}
export interface LocalReconciliationCompletionSecretConfigBinding {
readonly secretConfigId: string;
readonly decisionId: string;
readonly expectedApplyDigest: string;
}
export interface LocalReconciliationCompletionRunHistoryBinding {
readonly preservationId: string;
readonly expectedPreservationDigest: string;
}
export interface LocalReconciliationCompleteCommand {
readonly schemaVersion: 1 | 2;
readonly schemaVersion: 1 | 2 | 3;
readonly operation: 'local.deployment.reconciliation.complete';
readonly options: Readonly<LocalReconciliationCompletionOptions>;
readonly request: Readonly<{
@@ -52,13 +66,14 @@ export interface LocalReconciliationCompleteCommand {
expectedApplicationPlanDigest: string;
expectedHeadDigest: string;
automation: Readonly<LocalReconciliationCompletionAutomationBinding> | null;
secretConfig: Readonly<LocalReconciliationCompletionSecretConfigBinding> | null;
runHistory: Readonly<LocalReconciliationCompletionRunHistoryBinding> | null;
completedAtMs: number;
}>;
}
export interface LocalReconciliationCompletionVerifyCommand {
readonly schemaVersion: 1 | 2;
readonly schemaVersion: 1 | 2 | 3;
readonly operation: 'local.deployment.reconciliation.complete.verify';
readonly options: Readonly<LocalReconciliationCompletionOptions>;
readonly request: Readonly<{
@@ -66,6 +81,7 @@ export interface LocalReconciliationCompletionVerifyCommand {
applicationId: string;
expectedCompletionDigest: string;
automation: Readonly<LocalReconciliationCompletionAutomationBinding> | null;
secretConfig: Readonly<LocalReconciliationCompletionSecretConfigBinding> | null;
runHistory: Readonly<LocalReconciliationCompletionRunHistoryBinding> | null;
}>;
}
@@ -81,7 +97,7 @@ export interface LocalReconciliationCompletionResult {
readonly applicationId: string;
readonly completionDigest: string;
readonly domainCount: 8;
readonly adapterCount: 0 | 1 | 2;
readonly adapterCount: 0 | 1 | 2 | 3;
readonly instanceHeadDigest: string;
}
@@ -180,9 +196,41 @@ function normalizeAutomationOptions(
});
}
function normalizeSecretConfigOptions(
value: unknown,
): Readonly<LocalReconciliationCompletionSecretConfigOptions> | null {
if (value === null) return null;
const selected = record(value, 'secret config options');
exact(
selected,
[
'secretConfigApplyRoot',
'secretConfigDecisionRoot',
'secretConfigRoot',
'targetDatabasePath',
],
'secret config options',
);
return Object.freeze({
secretConfigRoot: safePath(selected.secretConfigRoot, 'secretConfigRoot'),
secretConfigDecisionRoot: safePath(
selected.secretConfigDecisionRoot,
'secretConfigDecisionRoot',
),
secretConfigApplyRoot: safePath(
selected.secretConfigApplyRoot,
'secretConfigApplyRoot',
),
targetDatabasePath: safePath(
selected.targetDatabasePath,
'targetDatabasePath',
),
});
}
function normalizeOptions(
value: unknown,
schemaVersion: 1 | 2,
schemaVersion: 1 | 2 | 3,
): Readonly<LocalReconciliationCompletionOptions> {
const selected = record(value, 'options');
exact(
@@ -195,6 +243,15 @@ function normalizeOptions(
'completionRoot',
'deploymentRoot',
]
: schemaVersion === 2
? [
'allowRootService',
'applicationRoot',
'automation',
'completionRoot',
'deploymentRoot',
'runHistory',
]
: [
'allowRootService',
'applicationRoot',
@@ -202,6 +259,7 @@ function normalizeOptions(
'completionRoot',
'deploymentRoot',
'runHistory',
'secretConfig',
],
'options',
);
@@ -212,8 +270,14 @@ function normalizeOptions(
fail('command identity is invalid');
}
const automation = normalizeAutomationOptions(selected.automation);
const secretConfig =
schemaVersion === 3
? normalizeSecretConfigOptions(selected.secretConfig)
: null;
const runHistory =
schemaVersion === 1
? null
: schemaVersion === 3 && selected.runHistory === null
? null
: normalizeRunHistoryOptions(selected.runHistory);
const normalized = Object.freeze({
@@ -221,6 +285,7 @@ function normalizeOptions(
applicationRoot: safePath(selected.applicationRoot, 'applicationRoot'),
completionRoot: safePath(selected.completionRoot, 'completionRoot'),
automation,
secretConfig,
runHistory,
allowRootService: selected.allowRootService,
}) as Readonly<LocalReconciliationCompletionOptions>;
@@ -235,6 +300,13 @@ function normalizeOptions(
automation.automationDecisionRoot,
automation.automationApplyRoot,
]),
...(secretConfig === null
? []
: [
secretConfig.secretConfigRoot,
secretConfig.secretConfigDecisionRoot,
secretConfig.secretConfigApplyRoot,
]),
...(runHistory === null ? [] : [runHistory.runHistoryRoot]),
];
for (let left = 0; left < roots.length; left += 1) {
@@ -257,6 +329,23 @@ function normalizeOptions(
) {
fail('targetDatabasePath overlaps an authority root');
}
if (
secretConfig !== null &&
roots.some(
(root) =>
overlaps(root, secretConfig.targetDatabasePath) ||
overlaps(secretConfig.targetDatabasePath, root),
)
) {
fail('targetDatabasePath overlaps an authority root');
}
if (
automation !== null &&
secretConfig !== null &&
automation.targetDatabasePath !== secretConfig.targetDatabasePath
) {
fail('adapter targetDatabasePath values differ');
}
if (
runHistory !== null &&
roots.some(
@@ -305,6 +394,30 @@ function normalizeAutomationBinding(
});
}
function normalizeSecretConfigBinding(
value: unknown,
): Readonly<LocalReconciliationCompletionSecretConfigBinding> | null {
if (value === null) return null;
const selected = record(value, 'secret config binding');
exact(
selected,
['decisionId', 'expectedApplyDigest', 'secretConfigId'],
'secret config binding',
);
return Object.freeze({
secretConfigId: identifier(
selected.secretConfigId,
UUID_V4,
'secretConfigId',
),
decisionId: identifier(selected.decisionId, UUID_V7, 'decisionId'),
expectedApplyDigest: digest(
selected.expectedApplyDigest,
'expectedApplyDigest',
),
});
}
function normalizeRunHistoryBinding(
value: unknown,
): Readonly<LocalReconciliationCompletionRunHistoryBinding> {
@@ -335,7 +448,9 @@ function command(value: unknown, operation: string) {
'command',
);
if (
(selected.schemaVersion !== 1 && selected.schemaVersion !== 2) ||
(selected.schemaVersion !== 1 &&
selected.schemaVersion !== 2 &&
selected.schemaVersion !== 3) ||
selected.operation !== operation
) {
fail('command version or operation is invalid');
@@ -362,6 +477,16 @@ export function normalizeLocalReconciliationCompleteCommand(
'expectedApplicationPlanDigest',
'expectedHeadDigest',
]
: selected.schemaVersion === 2
? [
'applicationId',
'automation',
'completedAtMs',
'completionId',
'expectedApplicationPlanDigest',
'expectedHeadDigest',
'runHistory',
]
: [
'applicationId',
'automation',
@@ -370,6 +495,7 @@ export function normalizeLocalReconciliationCompleteCommand(
'expectedApplicationPlanDigest',
'expectedHeadDigest',
'runHistory',
'secretConfig',
],
'request',
);
@@ -403,8 +529,14 @@ export function normalizeLocalReconciliationCompleteCommand(
'expectedHeadDigest',
),
automation: normalizeAutomationBinding(selected.request.automation),
secretConfig:
selected.schemaVersion === 3
? normalizeSecretConfigBinding(selected.request.secretConfig)
: null,
runHistory:
selected.schemaVersion === 1
? null
: selected.schemaVersion === 3 && selected.request.runHistory === null
? null
: normalizeRunHistoryBinding(selected.request.runHistory),
completedAtMs: selected.request.completedAtMs as number,
@@ -428,12 +560,21 @@ export function normalizeLocalReconciliationCompletionVerifyCommand(
'completionId',
'expectedCompletionDigest',
]
: selected.schemaVersion === 2
? [
'applicationId',
'automation',
'completionId',
'expectedCompletionDigest',
'runHistory',
]
: [
'applicationId',
'automation',
'completionId',
'expectedCompletionDigest',
'runHistory',
'secretConfig',
],
'request',
);
@@ -457,8 +598,14 @@ export function normalizeLocalReconciliationCompletionVerifyCommand(
'expectedCompletionDigest',
),
automation: normalizeAutomationBinding(selected.request.automation),
secretConfig:
selected.schemaVersion === 3
? normalizeSecretConfigBinding(selected.request.secretConfig)
: null,
runHistory:
selected.schemaVersion === 1
? null
: selected.schemaVersion === 3 && selected.request.runHistory === null
? null
: normalizeRunHistoryBinding(selected.request.runHistory),
}),
@@ -33,6 +33,20 @@ import type {
LocalReconciliationAutomationApplyReceipt,
} from '../application/automation/applyEvidence';
import { readLocalReconciliationAutomationDecisionTerminal } from '../application/automation/decisionCoordinator';
import { readLocalReconciliationSecretConfigDecisionTerminal } from '../application/secret-and-config/decisionCoordinator';
import type {
LocalReconciliationSecretConfigApplyIntent,
LocalReconciliationSecretConfigApplyReceipt,
} from '../application/secret-and-config/application/evidence';
import {
collectLocalReconciliationSecretConfigCompletedStorage,
localReconciliationSecretConfigApplyPaths,
readLocalReconciliationSecretConfigApplyIntent,
readLocalReconciliationSecretConfigApplyReceipt,
validateLocalReconciliationSecretConfigAppliedStorage,
validateLocalReconciliationSecretConfigApplyCatalog,
validateLocalReconciliationSecretConfigCompletedStorage,
} from '../application/secret-and-config/application/storage';
import {
readLocalReconciliationRunHistoryTerminal,
type LocalReconciliationRunHistoryDependencies,
@@ -66,6 +80,14 @@ interface AutomationProof {
readonly intent: Readonly<LocalReconciliationAutomationApplyIntent>;
readonly receipt: Readonly<LocalReconciliationAutomationApplyReceipt>;
readonly paths: ReturnType<typeof localReconciliationAutomationApplyPaths>;
readonly storageState: 'applied' | 'completed';
}
interface SecretConfigProof {
readonly intent: Readonly<LocalReconciliationSecretConfigApplyIntent>;
readonly receipt: Readonly<LocalReconciliationSecretConfigApplyReceipt>;
readonly paths: ReturnType<typeof localReconciliationSecretConfigApplyPaths>;
readonly storageState: 'applied' | 'completed';
}
interface RunHistoryProof {
@@ -79,6 +101,7 @@ export interface LocalReconciliationCompletionDependencies
readonly afterTerminalSealed?: () => void;
readonly afterHeadAdvanced?: () => void;
readonly afterBackupCollected?: () => void;
readonly afterSecretConfigBackupCollected?: () => void;
}
async function runHistoryProof(
@@ -444,6 +467,134 @@ async function automationProof(
) {
fail('automation apply evidence is detached');
}
if (command.request.secretConfig === null) {
const current = await (
dependencies.inspectSnapshot ?? inspectLocalSqliteSnapshot
)({
databasePath: options.targetDatabasePath,
profile: intent.profile,
});
if (current.sha256 !== receipt.targetAfter.sha256) {
fail('automation target drifted after apply');
}
}
const storageState = fs.existsSync(selected.backup)
? ('applied' as const)
: ('completed' as const);
if (storageState === 'applied') {
validateLocalReconciliationAutomationAppliedStorage(selected, intent, uid);
} else {
validateLocalReconciliationAutomationCompletedStorage(selected, uid);
}
return Object.freeze({
intent,
receipt,
paths: selected,
storageState,
});
}
async function secretConfigProof(
command: Readonly<LocalReconciliationCompleteCommand>,
terminal: Readonly<LocalReconciliationApplicationTerminal>,
automation: Readonly<AutomationProof> | null,
uid: number,
dependencies: LocalReconciliationCompletionDependencies,
): Promise<Readonly<SecretConfigProof> | null> {
const secretConfigDomain = terminal.plan.domains.find(
(domain) => domain.domain === 'secret_and_config',
);
if (!secretConfigDomain) fail('secret and config domain is absent');
if (secretConfigDomain.action === 'no_effect') {
if (
command.options.secretConfig !== null ||
command.request.secretConfig !== null
) {
fail('no-effect completion must not carry secret config authority');
}
return null;
}
if (
secretConfigDomain.action === 'manual_external' &&
command.options.secretConfig === null &&
command.request.secretConfig === null
) {
return null;
}
if (
secretConfigDomain.action !== 'manual_external' ||
command.options.secretConfig === null ||
command.request.secretConfig === null
) {
fail('secret and config domain is not terminally provable');
}
const options = command.options.secretConfig;
const binding = command.request.secretConfig;
for (const [directory, label] of [
[options.secretConfigRoot, 'secretConfigRoot'],
[options.secretConfigDecisionRoot, 'secretConfigDecisionRoot'],
[options.secretConfigApplyRoot, 'secretConfigApplyRoot'],
] as const) {
validatePrivateDirectory(directory, uid, label);
}
const decision = await readLocalReconciliationSecretConfigDecisionTerminal(
{
deploymentRoot: command.options.deploymentRoot,
applicationRoot: command.options.applicationRoot,
secretConfigRoot: options.secretConfigRoot,
secretConfigDecisionRoot: options.secretConfigDecisionRoot,
allowRootService: command.options.allowRootService,
},
binding.secretConfigId,
uid,
[
'reconciliation_secret_config_applied',
'reconciliation_secret_config_rolled_back',
'reconciliation_completed',
],
);
if (
decision.receipt.decisionId !== binding.decisionId ||
decision.receipt.outcome !== 'ready' ||
decision.context.application.plan.applicationPlanDigest !==
terminal.plan.applicationPlanDigest
) {
fail('secret config decision is detached from application authority');
}
const selected = localReconciliationSecretConfigApplyPaths(
options.secretConfigApplyRoot,
binding.secretConfigId,
);
validateLocalReconciliationSecretConfigApplyCatalog(selected);
const intent = readLocalReconciliationSecretConfigApplyIntent(selected, uid);
const receipt = readLocalReconciliationSecretConfigApplyReceipt(
selected,
uid,
);
if (
intent.command.options.deploymentRoot !== command.options.deploymentRoot ||
intent.command.options.applicationRoot !==
command.options.applicationRoot ||
intent.command.options.secretConfigRoot !== options.secretConfigRoot ||
intent.command.options.secretConfigDecisionRoot !==
options.secretConfigDecisionRoot ||
intent.command.options.secretConfigApplyRoot !==
options.secretConfigApplyRoot ||
intent.command.options.targetDatabasePath !== options.targetDatabasePath ||
intent.command.request.secretConfigId !== binding.secretConfigId ||
intent.command.request.decisionId !== binding.decisionId ||
intent.command.request.expectedDecisionDigest !==
decision.receipt.decisionDigest ||
receipt.secretConfigId !== binding.secretConfigId ||
receipt.decisionId !== binding.decisionId ||
receipt.applyDigest !== binding.expectedApplyDigest ||
receipt.preparationDigest !== intent.preparationDigest ||
(automation !== null &&
intent.backup.sha256 !== automation.receipt.targetAfter.sha256) ||
fs.existsSync(selected.rollbackReceipt)
) {
fail('secret config apply evidence is detached');
}
const current = await (
dependencies.inspectSnapshot ?? inspectLocalSqliteSnapshot
)({
@@ -451,14 +602,36 @@ async function automationProof(
profile: intent.profile,
});
if (current.sha256 !== receipt.targetAfter.sha256) {
fail('automation target drifted after apply');
fail('secret config target drifted after apply');
}
return Object.freeze({ intent, receipt, paths: selected });
const storageState = fs.existsSync(selected.backup)
? ('applied' as const)
: ('completed' as const);
if (storageState === 'applied') {
validateLocalReconciliationSecretConfigAppliedStorage(
selected,
intent,
uid,
);
} else {
validateLocalReconciliationSecretConfigCompletedStorage(
selected,
intent,
uid,
);
}
return Object.freeze({
intent,
receipt,
paths: selected,
storageState,
});
}
function domainEvidence(
terminal: Readonly<LocalReconciliationApplicationTerminal>,
automation: Readonly<AutomationProof> | null,
secretConfig: Readonly<SecretConfigProof> | null,
runHistory: Readonly<RunHistoryProof> | null,
): readonly Readonly<LocalReconciliationCompletionDomainEvidence>[] {
return Object.freeze(
@@ -483,6 +656,18 @@ function domainEvidence(
evidenceDigest: automation.receipt.applyDigest,
});
}
if (
domain.domain === 'secret_and_config' &&
domain.action === 'manual_external' &&
secretConfig !== null
) {
return Object.freeze({
domain: domain.domain,
action: 'adapter_required' as const,
evidenceKind: 'secret_config_application' as const,
evidenceDigest: secretConfig.receipt.applyDigest,
});
}
if (
domain.domain === 'run_history' &&
domain.action === 'adapter_required' &&
@@ -571,13 +756,20 @@ function assertSourceHead(
head: Readonly<LocalCutoverInstanceHead>,
expectedHeadDigest: string,
automation: Readonly<AutomationProof> | null,
secretConfig: Readonly<SecretConfigProof> | null,
): void {
const expectedState =
automation === null
secretConfig !== null
? 'reconciliation_secret_config_applied'
: automation === null
? 'reconciliation_application_planned'
: 'reconciliation_automation_applied';
const expectedSource =
automation === null ? undefined : automation.receipt.applyDigest;
secretConfig !== null
? secretConfig.receipt.applyDigest
: automation === null
? undefined
: automation.receipt.applyDigest;
if (
head.headDigest !== expectedHeadDigest ||
head.state !== expectedState ||
@@ -618,13 +810,25 @@ export async function completeLocalReconciliation(
uid,
dependencies,
);
const secretConfig = await secretConfigProof(
command,
terminal,
automation,
uid,
dependencies,
);
const runHistory = await runHistoryProof(
command,
terminal,
uid,
dependencies,
);
const domains = domainEvidence(terminal, automation, runHistory);
const domains = domainEvidence(
terminal,
automation,
secretConfig,
runHistory,
);
const selected = ensureCompletionDirectory(
command.options.completionRoot,
command.request.completionId,
@@ -654,13 +858,19 @@ export async function completeLocalReconciliation(
fail('completion command is not an exact replay');
}
} else {
assertSourceHead(head, command.request.expectedHeadDigest, automation);
assertSourceHead(
head,
command.request.expectedHeadDigest,
automation,
secretConfig,
);
const adapterCount = domains.filter(
(domain) => domain.action === 'adapter_required',
).length as 0 | 1 | 2;
).length as 0 | 1 | 2 | 3;
const latestEvidenceAtMs = Math.max(
terminal.plan.committedAtMs,
automation?.receipt.appliedAtMs ?? 0,
secretConfig?.receipt.appliedAtMs ?? 0,
runHistory?.receipt.preservedAtMs ?? 0,
);
if (command.request.completedAtMs < latestEvidenceAtMs) {
@@ -705,13 +915,12 @@ export async function completeLocalReconciliation(
uid,
);
if (head.state !== 'reconciliation_completed') {
assertSourceHead(head, receipt.sourceHeadDigest, automation);
if (automation !== null) {
validateLocalReconciliationAutomationAppliedStorage(
automation.paths,
automation.intent,
uid,
);
assertSourceHead(head, receipt.sourceHeadDigest, automation, secretConfig);
if (automation?.storageState === 'completed') {
fail('automation rollback backup was collected before completion');
}
if (secretConfig?.storageState === 'completed') {
fail('secret config rollback backup was collected before completion');
}
head = advanceCompletedHead(terminal, receipt, uid);
} else if (head.sourceRecordDigest !== receipt.completionDigest) {
@@ -726,6 +935,14 @@ export async function completeLocalReconciliation(
);
dependencies.afterBackupCollected?.();
}
if (secretConfig !== null) {
collectLocalReconciliationSecretConfigCompletedStorage(
secretConfig.paths,
secretConfig.intent,
uid,
);
dependencies.afterSecretConfigBackupCollected?.();
}
return result(command.operation, status, receipt, head);
}
@@ -777,6 +994,7 @@ export async function verifyLocalReconciliationCompletion(
expectedApplicationPlanDigest: receipt.applicationPlanDigest,
expectedHeadDigest: receipt.sourceHeadDigest,
automation: command.request.automation,
secretConfig: command.request.secretConfig,
runHistory: command.request.runHistory,
completedAtMs: receipt.completedAtMs,
}),
@@ -787,13 +1005,25 @@ export async function verifyLocalReconciliationCompletion(
uid,
dependencies,
);
const secretConfig = await secretConfigProof(
syntheticCompleteCommand,
terminal,
automation,
uid,
dependencies,
);
const runHistory = await runHistoryProof(
syntheticCompleteCommand,
terminal,
uid,
dependencies,
);
const domains = domainEvidence(terminal, automation, runHistory);
const domains = domainEvidence(
terminal,
automation,
secretConfig,
runHistory,
);
validateReceiptBinding(
receipt,
terminal,
@@ -818,6 +1048,13 @@ export async function verifyLocalReconciliationCompletion(
uid,
);
}
if (secretConfig !== null) {
validateLocalReconciliationSecretConfigCompletedStorage(
secretConfig.paths,
secretConfig.intent,
uid,
);
}
return result(command.operation, 'verified', receipt, head);
}
@@ -16,13 +16,14 @@ export interface LocalReconciliationCompletionDomainEvidence {
readonly evidenceKind:
| 'application_summary'
| 'automation_apply'
| 'run_history_preservation';
| 'run_history_preservation'
| 'secret_config_application';
readonly evidenceDigest: string;
}
export interface LocalReconciliationCompletionReceipt {
readonly schema: typeof RECEIPT_SCHEMA;
readonly schemaVersion: 1 | 2;
readonly schemaVersion: 1 | 2 | 3;
readonly state: 'reconciliation_completed';
readonly completionId: string;
readonly applicationId: string;
@@ -34,7 +35,7 @@ export interface LocalReconciliationCompletionReceipt {
readonly applicationPlanDigest: string;
readonly sourceHeadDigest: string;
readonly domains: readonly Readonly<LocalReconciliationCompletionDomainEvidence>[];
readonly adapterCount: 0 | 1 | 2;
readonly adapterCount: 0 | 1 | 2 | 3;
readonly completedAtMs: number;
readonly completionDigest: string;
}
@@ -68,7 +69,7 @@ function exact(
function domainEvidence(
value: unknown,
expectedDomain: LocalReconciliationPlanDomain,
schemaVersion: 1 | 2,
schemaVersion: 1 | 2 | 3,
): Readonly<LocalReconciliationCompletionDomainEvidence> {
const selected = exact(
value,
@@ -83,13 +84,18 @@ function domainEvidence(
selected.action === 'adapter_required' &&
selected.evidenceKind === 'automation_apply';
const runHistory =
schemaVersion === 2 &&
schemaVersion >= 2 &&
expectedDomain === 'run_history' &&
selected.action === 'adapter_required' &&
selected.evidenceKind === 'run_history_preservation';
const secretConfig =
schemaVersion === 3 &&
expectedDomain === 'secret_and_config' &&
selected.action === 'adapter_required' &&
selected.evidenceKind === 'secret_config_application';
if (
selected.domain !== expectedDomain ||
(!noEffect && !automation && !runHistory) ||
(!noEffect && !automation && !runHistory && !secretConfig) ||
typeof selected.evidenceDigest !== 'string' ||
!DIGEST.test(selected.evidenceDigest)
) {
@@ -110,8 +116,12 @@ export function buildLocalReconciliationCompletionReceipt(
>,
): Readonly<LocalReconciliationCompletionReceipt> {
const schemaVersion = input.domains.some(
(domain) => domain.evidenceKind === 'run_history_preservation',
(domain) => domain.evidenceKind === 'secret_config_application',
)
? (3 as const)
: input.domains.some(
(domain) => domain.evidenceKind === 'run_history_preservation',
)
? (2 as const)
: (1 as const);
const payload = Object.freeze({
@@ -154,7 +164,11 @@ export function normalizeLocalReconciliationCompletionReceipt(
if (!Array.isArray(selected.domains) || selected.domains.length !== 8) {
fail('receipt domain catalog is invalid');
}
if (selected.schemaVersion !== 1 && selected.schemaVersion !== 2) {
if (
selected.schemaVersion !== 1 &&
selected.schemaVersion !== 2 &&
selected.schemaVersion !== 3
) {
fail('receipt schema version is invalid');
}
const schemaVersion = selected.schemaVersion;
@@ -171,10 +185,23 @@ export function normalizeLocalReconciliationCompletionReceipt(
const normalized = Object.freeze({ ...raw, domains });
if (
selected.schema !== RECEIPT_SCHEMA ||
(schemaVersion === 2) !==
domains.some(
(schemaVersion === 1 &&
domains.some((domain) =>
['run_history_preservation', 'secret_config_application'].includes(
domain.evidenceKind,
),
)) ||
(schemaVersion === 2 &&
(!domains.some(
(domain) => domain.evidenceKind === 'run_history_preservation',
) ||
domains.some(
(domain) => domain.evidenceKind === 'secret_config_application',
))) ||
(schemaVersion === 3 &&
!domains.some(
(domain) => domain.evidenceKind === 'secret_config_application',
)) ||
selected.state !== 'reconciliation_completed' ||
typeof selected.completionId !== 'string' ||
!UUID_V4.test(selected.completionId) ||
@@ -195,7 +222,7 @@ export function normalizeLocalReconciliationCompletionReceipt(
].every(
(candidate) => typeof candidate === 'string' && DIGEST.test(candidate),
) ||
![0, 1, 2].includes(selected.adapterCount as number) ||
![0, 1, 2, 3].includes(selected.adapterCount as number) ||
selected.adapterCount !== adapterCount ||
!Number.isSafeInteger(selected.completedAtMs) ||
(selected.completedAtMs as number) < 0 ||