fix(ql3): bind evolved reconciliation target

This commit is contained in:
whyour
2026-09-02 08:19:44 +08:00
parent a8ca739fe8
commit cbdddffc91
8 changed files with 316 additions and 34 deletions
@@ -223,12 +223,25 @@ function normalizeOptions(
fail('authentication or Secret material must be below deploymentRoot'); fail('authentication or Secret material must be below deploymentRoot');
} }
} }
const targetRelative = path.relative(
normalized.deploymentRoot,
normalized.targetDatabasePath,
);
if ( if (
roots.some( !targetRelative ||
(root) => targetRelative.startsWith('..') ||
overlaps(root, normalized.targetDatabasePath) || path.isAbsolute(targetRelative)
overlaps(normalized.targetDatabasePath, root), ) {
) fail('targetDatabasePath must be below deploymentRoot');
}
if (
roots
.slice(1)
.some(
(root) =>
overlaps(root, normalized.targetDatabasePath) ||
overlaps(normalized.targetDatabasePath, root),
)
) { ) {
fail('targetDatabasePath overlaps an authority root'); fail('targetDatabasePath overlaps an authority root');
} }
@@ -338,6 +338,14 @@ export async function applyLocalReconciliationSecretConfig(
) { ) {
fail('apply command is detached from a ready signed decision'); fail('apply command is detached from a ready signed decision');
} }
const targetSnapshotSha256 = terminal.context.planHeader.targetSnapshotSha256;
if (terminal.receipt.applyBindingCount > 0 && targetSnapshotSha256 === null) {
fail('active binding plan is missing evolved target authority');
}
const stoppedProofOptions =
targetSnapshotSha256 === null
? {}
: { expectedEvolvedTargetSha256: targetSnapshotSha256 };
const planTerminal = terminal.context.planTerminal; const planTerminal = terminal.context.planTerminal;
const capture = readLocalReconciliationCaptureIntent( const capture = readLocalReconciliationCaptureIntent(
planTerminal.intent.command.options.captureRoot, planTerminal.intent.command.options.captureRoot,
@@ -375,7 +383,11 @@ export async function applyLocalReconciliationSecretConfig(
fail('apply lost reviewed head compare-and-swap'); fail('apply lost reviewed head compare-and-swap');
} }
discardUnpreparedLocalReconciliationSecretConfigMaterials(selected); discardUnpreparedLocalReconciliationSecretConfigMaterials(selected);
const before = proveLocalReconciliationStoppedState(capture.command, uid); const before = proveLocalReconciliationStoppedState(
capture.command,
uid,
stoppedProofOptions,
);
const materials: Readonly<PreparedReconciliationSecretConfigMaterial>[] = const materials: Readonly<PreparedReconciliationSecretConfigMaterial>[] =
[]; [];
const openRequirements = const openRequirements =
@@ -441,7 +453,11 @@ export async function applyLocalReconciliationSecretConfig(
? {} ? {}
: { busyTimeoutMs: command.options.busyTimeoutMs }), : { busyTimeoutMs: command.options.busyTimeoutMs }),
}); });
const after = proveLocalReconciliationStoppedState(capture.command, uid); const after = proveLocalReconciliationStoppedState(
capture.command,
uid,
stoppedProofOptions,
);
if (after.proofDigest !== before.proofDigest) { if (after.proofDigest !== before.proofDigest) {
fail('stopped target drifted across preparation'); fail('stopped target drifted across preparation');
} }
@@ -522,7 +538,11 @@ export async function applyLocalReconciliationSecretConfig(
fail('apply lost prepared head compare-and-swap'); fail('apply lost prepared head compare-and-swap');
} }
if (!recoveringPreparedIntent) { if (!recoveringPreparedIntent) {
const stopped = proveLocalReconciliationStoppedState(capture.command, uid); const stopped = proveLocalReconciliationStoppedState(
capture.command,
uid,
stoppedProofOptions,
);
if (stopped.proofDigest !== intent.stoppedProofDigest) { if (stopped.proofDigest !== intent.stoppedProofDigest) {
fail('stopped proof drifted before write'); fail('stopped proof drifted before write');
} }
@@ -614,6 +614,7 @@ function publishPlan(
authority: Readonly<SecretConfigReviewAuthority>, authority: Readonly<SecretConfigReviewAuthority>,
dependencies: LocalReconciliationSecretConfigPlanDependencies, dependencies: LocalReconciliationSecretConfigPlanDependencies,
uid: number, uid: number,
targetSnapshotSha256: string | null,
automationTarget?: DatabaseSync, automationTarget?: DatabaseSync,
): Readonly<LocalReconciliationSecretConfigPlanReceipt> { ): Readonly<LocalReconciliationSecretConfigPlanReceipt> {
let descriptor: number | undefined; let descriptor: number | undefined;
@@ -649,6 +650,7 @@ function publishPlan(
projectId: command.request.projectId, projectId: command.request.projectId,
tableDisposition: authority.tableDisposition, tableDisposition: authority.tableDisposition,
unadaptedLegacyConfigCount: authority.unadaptedLegacyConfigCount, unadaptedLegacyConfigCount: authority.unadaptedLegacyConfigCount,
targetSnapshotSha256,
preparedHeadDigest: head.headDigest, preparedHeadDigest: head.headDigest,
preparedAtMs: command.request.preparedAtMs, preparedAtMs: command.request.preparedAtMs,
}); });
@@ -987,6 +989,7 @@ export async function planLocalReconciliationSecretConfig(
authority, authority,
dependencies, dependencies,
identity.uid, identity.uid,
automationTarget?.snapshotSha256 ?? null,
target, target,
); );
const receipt = const receipt =
@@ -226,6 +226,7 @@ function header(
'schemaVersion', 'schemaVersion',
'secretConfigId', 'secretConfigId',
'tableDisposition', 'tableDisposition',
'targetSnapshotSha256',
'unadaptedLegacyConfigCount', 'unadaptedLegacyConfigCount',
], ],
'header', 'header',
@@ -244,6 +245,9 @@ function header(
record.projectId.length < 1 || record.projectId.length < 1 ||
(record.tableDisposition !== 'absent' && (record.tableDisposition !== 'absent' &&
record.tableDisposition !== 'manual_external') || record.tableDisposition !== 'manual_external') ||
(record.targetSnapshotSha256 !== null &&
(typeof record.targetSnapshotSha256 !== 'string' ||
!DIGEST_PATTERN.test(record.targetSnapshotSha256))) ||
!Number.isSafeInteger(record.unadaptedLegacyConfigCount) || !Number.isSafeInteger(record.unadaptedLegacyConfigCount) ||
(record.unadaptedLegacyConfigCount as number) < 0 || (record.unadaptedLegacyConfigCount as number) < 0 ||
![ ![
@@ -54,6 +54,7 @@ export interface LocalReconciliationSecretConfigPlanHeader {
readonly projectId: string; readonly projectId: string;
readonly tableDisposition: 'absent' | 'manual_external'; readonly tableDisposition: 'absent' | 'manual_external';
readonly unadaptedLegacyConfigCount: number; readonly unadaptedLegacyConfigCount: number;
readonly targetSnapshotSha256: string | null;
readonly preparedHeadDigest: string; readonly preparedHeadDigest: string;
readonly preparedAtMs: number; readonly preparedAtMs: number;
readonly headerDigest: string; readonly headerDigest: string;
@@ -350,11 +351,7 @@ function targetAutomationAdoptionProjection(
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/, /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
), ),
planDigest: adoptionText(row, 'planDigest', DIGEST_PATTERN), planDigest: adoptionText(row, 'planDigest', DIGEST_PATTERN),
inventoryDigest: adoptionText( inventoryDigest: adoptionText(row, 'inventoryDigest', DIGEST_PATTERN),
row,
'inventoryDigest',
DIGEST_PATTERN,
),
decisionDigest: adoptionText(row, 'decisionDigest', DIGEST_PATTERN), decisionDigest: adoptionText(row, 'decisionDigest', DIGEST_PATTERN),
receiptDigest: adoptionText(row, 'receiptDigest', DIGEST_PATTERN), receiptDigest: adoptionText(row, 'receiptDigest', DIGEST_PATTERN),
authorizationFileDigest: adoptionText( authorizationFileDigest: adoptionText(
@@ -372,11 +369,7 @@ function targetAutomationAdoptionProjection(
adoptedTriggerCount: selectedAdoptedTriggerCount, adoptedTriggerCount: selectedAdoptedTriggerCount,
skippedCount, skippedCount,
auditEventId: adoptionText(row, 'auditEventId', UUID_V4_PATTERN), auditEventId: adoptionText(row, 'auditEventId', UUID_V4_PATTERN),
createdAtMs: adoptionCount( createdAtMs: adoptionCount(row, 'createdAtMs', Number.MAX_SAFE_INTEGER),
row,
'createdAtMs',
Number.MAX_SAFE_INTEGER,
),
}); });
if (payload.auditEventId !== payload.mutationId) { if (payload.auditEventId !== payload.mutationId) {
fail('target Automation adoption audit binding drifted'); fail('target Automation adoption audit binding drifted');
@@ -1048,8 +1041,7 @@ export function buildLocalReconciliationSecretConfigPlanReceipt(
adoptedLegacyTriggerCount: footer.adoptedLegacyTriggerCount, adoptedLegacyTriggerCount: footer.adoptedLegacyTriggerCount,
adoptionProvenanceTaskCount: footer.adoptionProvenanceTaskCount, adoptionProvenanceTaskCount: footer.adoptionProvenanceTaskCount,
adoptionProvenanceTriggerCount: footer.adoptionProvenanceTriggerCount, adoptionProvenanceTriggerCount: footer.adoptionProvenanceTriggerCount,
automationAdoptionProvenanceState: automationAdoptionProvenanceState: footer.automationAdoptionProvenanceState,
footer.automationAdoptionProvenanceState,
unadaptedLegacyConfigCount: footer.unadaptedLegacyConfigCount, unadaptedLegacyConfigCount: footer.unadaptedLegacyConfigCount,
outcome: footer.outcome, outcome: footer.outcome,
preparedAtMs: header.preparedAtMs, preparedAtMs: header.preparedAtMs,
@@ -342,14 +342,25 @@ function normalizeOptions(
} }
if ( if (
secretConfig !== null && secretConfig !== null &&
roots.some( roots
(root) => .slice(1)
overlaps(root, secretConfig.targetDatabasePath) || .some(
overlaps(secretConfig.targetDatabasePath, root), (root) =>
) overlaps(root, secretConfig.targetDatabasePath) ||
overlaps(secretConfig.targetDatabasePath, root),
)
) { ) {
fail('targetDatabasePath overlaps an authority root'); fail('targetDatabasePath overlaps an authority root');
} }
if (secretConfig !== null) {
const relative = path.relative(
normalized.deploymentRoot,
secretConfig.targetDatabasePath,
);
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
fail('Secret/Config targetDatabasePath must be below deploymentRoot');
}
}
if ( if (
automation !== null && automation !== null &&
secretConfig !== null && secretConfig !== null &&
@@ -28,9 +28,14 @@ const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
export interface LocalReconciliationStoppedProof { export interface LocalReconciliationStoppedProof {
readonly stoppedRecordDigest: string; readonly stoppedRecordDigest: string;
readonly reconciliationEvidenceDigest: string; readonly reconciliationEvidenceDigest: string;
readonly evolvedTargetSha256?: string;
readonly proofDigest: string; readonly proofDigest: string;
} }
export interface LocalReconciliationStoppedProofOptions {
readonly expectedEvolvedTargetSha256?: string;
}
function configurationError(message: string): never { function configurationError(message: string): never {
throw new LocalDeploymentConfigurationError(message); throw new LocalDeploymentConfigurationError(message);
} }
@@ -63,6 +68,61 @@ function exact(
} }
} }
function sameFileStat(left: fs.BigIntStats, right: fs.BigIntStats): boolean {
return (
left.dev === right.dev &&
left.ino === right.ino &&
left.mode === right.mode &&
left.nlink === right.nlink &&
left.uid === right.uid &&
left.size === right.size &&
left.mtimeNs === right.mtimeNs &&
left.ctimeNs === right.ctimeNs
);
}
function stableTargetSha256(filePath: string, uid: number): string {
let descriptor: number | undefined;
const buffer = Buffer.allocUnsafe(64 * 1024);
try {
const pathStat = fs.lstatSync(filePath, { bigint: true });
descriptor = fs.openSync(
filePath,
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW,
);
const before = fs.fstatSync(descriptor, { bigint: true });
if (
!pathStat.isFile() ||
pathStat.isSymbolicLink() ||
!sameFileStat(pathStat, before) ||
before.uid !== BigInt(uid) ||
before.nlink !== 1n ||
(before.mode & 0o077n) !== 0n ||
fs.realpathSync(filePath) !== filePath ||
before.size < 1n
) {
configurationError('evolved target database identity is invalid');
}
const hash = crypto.createHash('sha256');
for (;;) {
const count = fs.readSync(descriptor, buffer, 0, buffer.byteLength, null);
if (count === 0) break;
hash.update(buffer.subarray(0, count));
}
const after = fs.fstatSync(descriptor, { bigint: true });
if (!sameFileStat(before, after)) {
configurationError('evolved target database changed while hashing');
}
return hash.digest('hex');
} catch (error) {
if (error instanceof LocalDeploymentConfigurationError) throw error;
return configurationError('evolved target database is unavailable');
} finally {
buffer.fill(0);
if (descriptor !== undefined) fs.closeSync(descriptor);
}
}
function serviceManagerStoppedPath( function serviceManagerStoppedPath(
command: Readonly<LocalReconciliationCapturePrepareCommand>, command: Readonly<LocalReconciliationCapturePrepareCommand>,
): string { ): string {
@@ -177,7 +237,15 @@ function serviceManagerStoppedRecord(
export function proveLocalReconciliationStoppedState( export function proveLocalReconciliationStoppedState(
command: Readonly<LocalReconciliationCapturePrepareCommand>, command: Readonly<LocalReconciliationCapturePrepareCommand>,
uid: number, uid: number,
options: Readonly<LocalReconciliationStoppedProofOptions> = {},
): Readonly<LocalReconciliationStoppedProof> { ): Readonly<LocalReconciliationStoppedProof> {
const expectedEvolvedTargetSha256 = options.expectedEvolvedTargetSha256;
if (
expectedEvolvedTargetSha256 !== undefined &&
!DIGEST_PATTERN.test(expectedEvolvedTargetSha256)
) {
configurationError('evolved target snapshot digest is invalid');
}
const persisted = const persisted =
command.request.stoppedAuthority === 'docker' command.request.stoppedAuthority === 'docker'
? dockerStoppedEvidence(command) ? dockerStoppedEvidence(command)
@@ -198,11 +266,42 @@ export function proveLocalReconciliationStoppedState(
}, },
uid, uid,
); );
if ( const exactStoppedData =
current.disposition !== 'reconciliation_required' || expectedEvolvedTargetSha256 === undefined &&
(persisted !== undefined && current.disposition === 'reconciliation_required' &&
persisted.evidenceDigest !== current.evidenceDigest) (persisted === undefined ||
) { persisted.evidenceDigest === current.evidenceDigest);
let evolvedStoppedData = false;
if (expectedEvolvedTargetSha256 !== undefined) {
const currentSha256 = stableTargetSha256(
command.request.targetDatabasePath,
uid,
);
const confirmed = readTargetDataReconciliationEvidenceForPaths(
{
profile: command.request.profile,
activationPath: command.request.activationPath,
legacySourcePath: command.request.legacySourcePath,
targetDatabasePath: command.request.targetDatabasePath,
expectedActivationDigest: command.request.expectedActivationDigest,
...(adoptedTargetBaseline === undefined
? {}
: { adoptedTargetBaseline }),
},
uid,
);
evolvedStoppedData =
current.disposition === 'reconciliation_required' &&
current.sourceMatchesActivation === true &&
current.sourceSidecarsClear === true &&
current.targetSidecarsClear === true &&
current.targetMatchesActivation === false &&
(current.baselineKind !== 'adopted_target' ||
current.targetMatchesBaseline === false) &&
currentSha256 === expectedEvolvedTargetSha256 &&
confirmed.evidenceDigest === current.evidenceDigest;
}
if (!exactStoppedData && !evolvedStoppedData) {
configurationError( configurationError(
'stopped data does not have exact reconciliation-required evidence', 'stopped data does not have exact reconciliation-required evidence',
); );
@@ -211,6 +310,9 @@ export function proveLocalReconciliationStoppedState(
stoppedAuthority: command.request.stoppedAuthority, stoppedAuthority: command.request.stoppedAuthority,
stoppedRecordDigest: command.request.expectedStoppedRecordDigest, stoppedRecordDigest: command.request.expectedStoppedRecordDigest,
reconciliationEvidenceDigest: current.evidenceDigest, reconciliationEvidenceDigest: current.evidenceDigest,
...(expectedEvolvedTargetSha256 === undefined
? {}
: { evolvedTargetSha256: expectedEvolvedTargetSha256 }),
}); });
return Object.freeze({ ...payload, proofDigest: cutoverDigest(payload) }); return Object.freeze({ ...payload, proofDigest: cutoverDigest(payload) });
} }
@@ -1444,6 +1444,7 @@ async function secretConfigPlanFixture(t, options = {}) {
options.applicationId ?? '00000000-0000-4000-8000-000000000423', options.applicationId ?? '00000000-0000-4000-8000-000000000423',
reviewSuffix: `secret-config-${suffix}`, reviewSuffix: `secret-config-${suffix}`,
createDefaultSidecars: false, createDefaultSidecars: false,
targetInsideDeploymentRoot: true,
initializeDatabases: secretConfigDatabaseInitializer({ initializeDatabases: secretConfigDatabaseInitializer({
active: options.active === true, active: options.active === true,
configs: options.configs === true, configs: options.configs === true,
@@ -4278,6 +4279,129 @@ test('Secret/Config plan follows applied Automation and preserved Run History on
}, },
}); });
assert.equal(verified.status, 'verified'); assert.equal(verified.status, 'verified');
const secretConfigDecisionRoot = path.join(
path.dirname(state.captureRoot),
'cross-domain-secret-config-decision',
);
fs.mkdirSync(secretConfigDecisionRoot, { mode: 0o700 });
const candidates = fs
.readFileSync(
path.join(secretConfigRoot, secretConfigId, 'plan.ndjson'),
'utf8',
)
.trimEnd()
.split('\n')
.map((line) => JSON.parse(line))
.filter(
(record) =>
record.kind ===
'qinglong3-local-reconciliation-secret-config-plan-candidate',
);
const decisionState = {
...state,
planned,
secretConfigRoot,
secretConfigId,
secretConfigCommand,
secretConfigDecisionRoot,
candidates,
};
const secretConfigDecisionId = '019b0000-0000-7000-8000-000000000432';
const decisionPrepareCommand = secretConfigDecisionPrepareCommand(
decisionState,
secretConfigDecisionId,
);
const decisionPrepared = await prepareLocalReconciliationSecretConfigDecision(
decisionPrepareCommand,
);
const decisionFile = secretConfigDecisionFile(
decisionState,
{ result: decisionPrepared },
[
{
disposition: 'apply_active_binding',
reason: 'reviewed_active_binding',
},
],
'cross-domain-active-binding',
);
const decisionCommit = secretConfigDecisionCommitFixture(
decisionState,
{
result: decisionPrepared,
commandOptions: decisionPrepareCommand.options,
},
decisionFile.filePath,
);
const decision = await commitLocalReconciliationSecretConfigDecision(
decisionCommit.command,
decisionCommit.dependencies,
);
assert.equal(decision.outcome, 'ready');
assert.equal(decision.applyBindingCount, 1);
const secretKeyringPath = path.join(
state.deploymentRoot,
'cross-domain-local-secret-keyring.json',
);
await provisionLocalSecretKeyring(secretKeyringPath);
const secretConfigApplyRoot = path.join(
path.dirname(state.captureRoot),
'cross-domain-secret-config-apply',
);
fs.mkdirSync(secretConfigApplyRoot, { mode: 0o700 });
const appliedAtMs = decisionCommit.command.request.committedAtMs + 1;
const applied = await applyLocalReconciliationSecretConfig(
{
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.apply',
options: {
...decisionPrepareCommand.options,
secretConfigApplyRoot,
targetDatabasePath: state.targetDatabasePath,
secretKeyringPath,
ownerPepperKeyringDirectory:
state.command.options.ownerPepperKeyringDirectory,
credentialFilePath: state.command.options.credentialFilePath,
},
request: {
decisionId: secretConfigDecisionId,
secretConfigId,
expectedDecisionDigest: decision.decisionDigest,
expectedHeadDigest: decision.instanceHeadDigest,
mutationId: '00000000-0000-4000-8000-000000000433',
requestId: 'cross-domain-secret-config-apply',
appliedAtMs,
},
},
{
async openAuthenticationDatabase() {
return { async close() {} };
},
async authenticate(_database, authenticationOptions) {
const authenticatedAtMs = authenticationOptions.now();
return {
principal: {
subject: { type: 'user', id: 'review-owner' },
authenticationId: 'reconcile_secret_config_apply:test',
authenticatedAtMs,
expiresAtMs: authenticatedAtMs + 60 * 60 * 1_000,
assurance: 'local_console',
},
databaseFence: {
credentialId: 'review-owner',
credentialVersion: 1,
pepperKeyId: 'review-owner-v1',
pepperVersion: 1,
},
async confirm() {},
};
},
},
);
assert.equal(applied.state, 'reconciliation_secret_config_applied');
assert.equal(applied.activeBindingCount, 1);
}); });
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) => {
@@ -4515,10 +4639,7 @@ test('Secret/Config apply publishes encrypted material atomically and recovers e
}, },
async authenticate(_database, options) { async authenticate(_database, options) {
authentications += 1; authentications += 1;
assert.match( assert.match(options.authenticationNamespace, /^[a-z][a-z0-9_]{0,31}$/);
options.authenticationNamespace,
/^[a-z][a-z0-9_]{0,31}$/,
);
assert.equal( assert.equal(
options.authenticationNamespace, options.authenticationNamespace,
'reconcile_secret_config_apply', 'reconcile_secret_config_apply',
@@ -4561,6 +4682,22 @@ test('Secret/Config apply publishes encrypted material atomically and recovers e
), ),
/authentication or Secret material must be below deploymentRoot/, /authentication or Secret material must be below deploymentRoot/,
); );
await assert.rejects(
applyLocalReconciliationSecretConfig(
{
...applyCommand,
options: {
...applyOptions,
targetDatabasePath: path.join(
path.dirname(state.deploymentRoot),
'outside-target.sqlite',
),
},
},
applyDependencies,
),
/targetDatabasePath must be below deploymentRoot/,
);
for (const boundary of ['afterMaterialPublished']) { for (const boundary of ['afterMaterialPublished']) {
await assert.rejects( await assert.rejects(
applyLocalReconciliationSecretConfig(applyCommand, { applyLocalReconciliationSecretConfig(applyCommand, {