fix(ql3): bind secret planning to automation target

This commit is contained in:
whyour
2026-09-02 05:35:39 +08:00
parent 1cd298ebaf
commit f8ec9fccec
4 changed files with 613 additions and 56 deletions
@@ -16,10 +16,24 @@ export interface LocalReconciliationSecretConfigOptions {
readonly allowRootService: boolean;
}
export interface LocalReconciliationSecretConfigAutomationOptions
extends LocalReconciliationSecretConfigOptions {
readonly automationApplyRoot: string;
}
export interface LocalReconciliationSecretConfigAutomationBinding {
readonly automationId: string;
readonly decisionId: string;
readonly expectedApplyDigest: string;
}
export interface LocalReconciliationSecretConfigPlanCommand {
readonly schemaVersion: 1;
readonly schemaVersion: 1 | 2;
readonly operation: 'local.deployment.reconciliation.secret-config.plan';
readonly options: Readonly<LocalReconciliationSecretConfigOptions>;
readonly options: Readonly<
| LocalReconciliationSecretConfigOptions
| LocalReconciliationSecretConfigAutomationOptions
>;
readonly request: Readonly<{
secretConfigId: string;
applicationId: string;
@@ -28,6 +42,7 @@ export interface LocalReconciliationSecretConfigPlanCommand {
decisionFilePath: string;
projectId: string;
preparedAtMs: number;
automation?: Readonly<LocalReconciliationSecretConfigAutomationBinding>;
}>;
}
@@ -146,13 +161,18 @@ function projectId(value: unknown): string {
function normalizeOptions(
value: unknown,
): Readonly<LocalReconciliationSecretConfigOptions> {
schemaVersion: 1 | 2,
): Readonly<
| LocalReconciliationSecretConfigOptions
| LocalReconciliationSecretConfigAutomationOptions
> {
const options = object(value, 'options');
exact(
options,
[
'allowRootService',
'applicationRoot',
...(schemaVersion === 2 ? ['automationApplyRoot'] : []),
'deploymentRoot',
'secretConfigRoot',
],
@@ -169,6 +189,9 @@ function normalizeOptions(
safePath(options.deploymentRoot, 'deploymentRoot'),
safePath(options.applicationRoot, 'applicationRoot'),
safePath(options.secretConfigRoot, 'secretConfigRoot'),
...(schemaVersion === 2
? [safePath(options.automationApplyRoot, 'automationApplyRoot')]
: []),
];
for (let left = 0; left < roots.length; left += 1) {
for (let right = left + 1; right < roots.length; right += 1) {
@@ -184,6 +207,7 @@ function normalizeOptions(
deploymentRoot: roots[0]!,
applicationRoot: roots[1]!,
secretConfigRoot: roots[2]!,
...(schemaVersion === 2 ? { automationApplyRoot: roots[3]! } : {}),
allowRootService: options.allowRootService,
});
}
@@ -194,7 +218,11 @@ function command(
| LocalReconciliationSecretConfigPlanCommand['operation']
| LocalReconciliationSecretConfigVerifyCommand['operation'],
): Readonly<{
options: Readonly<LocalReconciliationSecretConfigOptions>;
schemaVersion: 1 | 2;
options: Readonly<
| LocalReconciliationSecretConfigOptions
| LocalReconciliationSecretConfigAutomationOptions
>;
request: Record<string, unknown>;
}> {
const selected = object(value, 'command');
@@ -203,15 +231,47 @@ function command(
['operation', 'options', 'request', 'schemaVersion'],
'command',
);
if (selected.schemaVersion !== 1 || selected.operation !== operation) {
const schemaVersion = selected.schemaVersion;
if (
(schemaVersion !== 1 &&
(schemaVersion !== 2 ||
operation !== 'local.deployment.reconciliation.secret-config.plan')) ||
selected.operation !== operation
) {
configurationError('command version or operation is invalid');
}
return Object.freeze({
options: normalizeOptions(selected.options),
schemaVersion,
options: normalizeOptions(selected.options, schemaVersion),
request: object(selected.request, 'request'),
});
}
function automationBinding(
value: unknown,
): Readonly<LocalReconciliationSecretConfigAutomationBinding> {
const selected = object(value, 'automation binding');
exact(
selected,
['automationId', 'decisionId', 'expectedApplyDigest'],
'automation binding',
);
return Object.freeze({
automationId: identifier(selected.automationId, 'automationId'),
decisionId:
typeof selected.decisionId === 'string' &&
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(
selected.decisionId,
)
? selected.decisionId
: configurationError('decisionId is invalid'),
expectedApplyDigest: digest(
selected.expectedApplyDigest,
'expectedApplyDigest',
),
});
}
export function normalizeLocalReconciliationSecretConfigPlanCommand(
value: unknown,
): Readonly<LocalReconciliationSecretConfigPlanCommand> {
@@ -223,6 +283,7 @@ export function normalizeLocalReconciliationSecretConfigPlanCommand(
selected.request,
[
'applicationId',
...(selected.schemaVersion === 2 ? ['automation'] : []),
'decisionFilePath',
'expectedApplicationPlanDigest',
'expectedHeadDigest',
@@ -255,7 +316,7 @@ export function normalizeLocalReconciliationSecretConfigPlanCommand(
configurationError('preparedAtMs is invalid');
}
return Object.freeze({
schemaVersion: 1,
schemaVersion: selected.schemaVersion,
operation: 'local.deployment.reconciliation.secret-config.plan',
options: selected.options,
request: Object.freeze({
@@ -278,6 +339,9 @@ export function normalizeLocalReconciliationSecretConfigPlanCommand(
decisionFilePath,
projectId: projectId(selected.request.projectId),
preparedAtMs: selected.request.preparedAtMs as number,
...(selected.schemaVersion === 2
? { automation: automationBinding(selected.request.automation) }
: {}),
}),
});
}
@@ -297,7 +361,12 @@ export function normalizeLocalReconciliationSecretConfigVerifyCommand(
return Object.freeze({
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.verify',
options: selected.options,
options: Object.freeze({
deploymentRoot: selected.options.deploymentRoot,
applicationRoot: selected.options.applicationRoot,
secretConfigRoot: selected.options.secretConfigRoot,
allowRootService: selected.options.allowRootService,
}),
request: Object.freeze({
secretConfigId: identifier(
selected.request.secretConfigId,
@@ -1,7 +1,9 @@
import fs from 'node:fs';
import path from 'node:path';
import { DatabaseSync } from 'node:sqlite';
import { readPrivateLocalCommandFile } from '@qinglong/local-command-file';
import { inspectLocalSqliteSnapshot } from '@qinglong/local-sqlite/rollout-safety';
import { currentIdentity } from '../../../foundation/contract';
import { LocalDeploymentConfigurationError } from '../../../foundation/error';
@@ -27,6 +29,12 @@ import {
readLocalReconciliationApplicationTerminal,
type LocalReconciliationApplicationTerminal,
} from '../coordinator';
import { verifyLocalReconciliationAutomationApply } from '../automation/applyCoordinator';
import {
localReconciliationAutomationApplyPaths,
readLocalReconciliationAutomationApplyIntent,
readLocalReconciliationAutomationApplyReceipt,
} from '../automation/applyStorage';
import {
normalizeLocalReconciliationSecretConfigPlanCommand,
normalizeLocalReconciliationSecretConfigVerifyCommand,
@@ -75,6 +83,12 @@ interface SecretConfigReviewAuthority {
readonly planTerminal: ReturnType<typeof readLocalReconciliationPlanTerminal>;
}
interface AutomationTargetAuthority {
readonly databasePath: string;
readonly profile: 'edge' | 'standalone';
readonly snapshotSha256: string;
}
function configurationError(message: string, cause?: unknown): never {
throw new LocalDeploymentConfigurationError(
`reconciliation secret config ${message}`,
@@ -304,14 +318,16 @@ function validateApplicationBinding(
terminal.plan.applicationId !== command.request.applicationId ||
terminal.plan.applicationPlanDigest !==
command.request.expectedApplicationPlanDigest ||
terminal.head.state !== 'reconciliation_application_planned' ||
terminal.head.sourceRecordDigest !== terminal.plan.applicationPlanDigest ||
!secretConfig ||
secretConfig.action !== 'manual_external' ||
terminal.head.state !== priorState ||
terminal.head.headDigest !== command.request.expectedHeadDigest ||
head.state !== priorState ||
head.headDigest !== command.request.expectedHeadDigest ||
(priorState === 'reconciliation_application_planned' &&
head.sourceRecordDigest !== terminal.plan.applicationPlanDigest) ||
(terminal.head.sourceRecordDigest !==
terminal.plan.applicationPlanDigest ||
head.sourceRecordDigest !== terminal.plan.applicationPlanDigest)) ||
command.request.preparedAtMs < terminal.plan.committedAtMs ||
command.request.preparedAtMs < head.updatedAtMs
) {
@@ -319,6 +335,176 @@ function validateApplicationBinding(
}
}
async function automationTargetAuthority(
command: Readonly<LocalReconciliationSecretConfigPlanCommand>,
terminal: Readonly<LocalReconciliationApplicationTerminal>,
head: Readonly<LocalCutoverInstanceHead>,
uid: number,
): Promise<Readonly<AutomationTargetAuthority> | null> {
if (expectedPriorState(terminal) === 'reconciliation_application_planned') {
if (
command.schemaVersion !== 1 ||
command.request.automation !== undefined
) {
configurationError('Automation target authority is unexpected');
}
return null;
}
if (
command.schemaVersion !== 2 ||
!('automationApplyRoot' in command.options) ||
command.request.automation === undefined
) {
configurationError('Automation target authority is required');
}
const automation = command.request.automation;
const automationApplyRoot = command.options.automationApplyRoot;
validatePrivateDirectory(automationApplyRoot, uid, 'automationApplyRoot');
const selected = localReconciliationAutomationApplyPaths(
automationApplyRoot,
automation.automationId,
);
const intent = readLocalReconciliationAutomationApplyIntent(selected, uid);
const receipt = readLocalReconciliationAutomationApplyReceipt(selected, uid);
if (
intent.command.options.automationApplyRoot !== automationApplyRoot ||
intent.command.options.deploymentRoot !== command.options.deploymentRoot ||
intent.command.options.applicationRoot !==
command.options.applicationRoot ||
intent.command.request.automationId !== automation.automationId ||
intent.command.request.decisionId !== automation.decisionId ||
intent.projectId !== command.request.projectId ||
receipt.automationId !== automation.automationId ||
receipt.decisionId !== automation.decisionId ||
receipt.applyDigest !== automation.expectedApplyDigest ||
receipt.applyDigest !== head.sourceRecordDigest ||
receipt.appliedAtMs > command.request.preparedAtMs
) {
configurationError('Automation target authority is detached');
}
const verified = await verifyLocalReconciliationAutomationApply({
schemaVersion: 1,
operation: 'local.deployment.reconciliation.automation.apply.verify',
options: intent.command.options,
request: {
decisionId: automation.decisionId,
automationId: automation.automationId,
expectedApplyDigest: automation.expectedApplyDigest,
},
});
if (
verified.state !== 'reconciliation_automation_applied' ||
verified.applyDigest !== automation.expectedApplyDigest ||
verified.instanceHeadDigest !== head.headDigest
) {
configurationError('Automation target authority did not verify');
}
return Object.freeze({
databasePath: intent.command.options.targetDatabasePath,
profile: intent.profile,
snapshotSha256: receipt.targetAfter.sha256,
});
}
function configureReadOnlyTarget(
client: DatabaseSync,
profile: 'edge' | 'standalone',
): void {
const cacheKiB = profile === 'edge' ? 2_048 : 8_192;
client.enableDefensive(true);
client.exec(
`PRAGMA trusted_schema = OFF; PRAGMA query_only = ON; PRAGMA temp_store = MEMORY; PRAGMA mmap_size = 0; PRAGMA cache_size = -${cacheKiB}`,
);
const trustedSchema = client.prepare('PRAGMA trusted_schema').get() as
| { readonly trusted_schema?: unknown }
| undefined;
const queryOnly = client.prepare('PRAGMA query_only').get() as
| { readonly query_only?: unknown }
| undefined;
const tempStore = client.prepare('PRAGMA temp_store').get() as
| { readonly temp_store?: unknown }
| undefined;
const mmapSize = client.prepare('PRAGMA mmap_size').get() as
| { readonly mmap_size?: unknown }
| undefined;
const cacheSize = client.prepare('PRAGMA cache_size').get() as
| { readonly cache_size?: unknown }
| undefined;
if (
trustedSchema?.trusted_schema !== 0 ||
queryOnly?.query_only !== 1 ||
tempStore?.temp_store !== 2 ||
mmapSize?.mmap_size !== 0 ||
cacheSize?.cache_size !== -cacheKiB
) {
configurationError('Automation target read-only configuration drifted');
}
}
async function withAutomationTargetDatabase<T>(
authority: Readonly<AutomationTargetAuthority>,
uid: number,
dependencies: LocalReconciliationSecretConfigPlanDependencies,
read: (database: DatabaseSync) => T,
): Promise<T> {
const beforeStat = fs.lstatSync(authority.databasePath, { bigint: true });
if (
!beforeStat.isFile() ||
beforeStat.isSymbolicLink() ||
Number(beforeStat.uid) !== uid ||
![0o600, 0o400].includes(Number(beforeStat.mode) & 0o777) ||
beforeStat.nlink !== 1n ||
fs.realpathSync(authority.databasePath) !== authority.databasePath
) {
configurationError('Automation target database identity is invalid');
}
const before = await inspectLocalSqliteSnapshot({
databasePath: authority.databasePath,
profile: authority.profile,
});
if (before.sha256 !== authority.snapshotSha256) {
configurationError('Automation target database snapshot drifted');
}
const cacheKiB = authority.profile === 'edge' ? 2_048 : 8_192;
dependencies.beforeDatabaseOpen?.('target', 'wal_shm_readonly', cacheKiB);
let client: DatabaseSync | undefined;
let output: T;
try {
client = new DatabaseSync(authority.databasePath, {
allowExtension: false,
defensive: true,
enableDoubleQuotedStringLiterals: false,
enableForeignKeyConstraints: true,
readOnly: true,
timeout: 0,
});
configureReadOnlyTarget(client, authority.profile);
output = read(client);
} catch (error) {
if (error instanceof LocalDeploymentConfigurationError) throw error;
return configurationError('Automation target database read failed', error);
} finally {
client?.close();
dependencies.afterDatabaseClose?.('target');
}
const afterStat = fs.lstatSync(authority.databasePath, { bigint: true });
const after = await inspectLocalSqliteSnapshot({
databasePath: authority.databasePath,
profile: authority.profile,
});
if (
afterStat.dev !== beforeStat.dev ||
afterStat.ino !== beforeStat.ino ||
afterStat.mtimeNs !== beforeStat.mtimeNs ||
afterStat.ctimeNs !== beforeStat.ctimeNs ||
after.sha256 !== before.sha256 ||
after.sha256 !== authority.snapshotSha256
) {
configurationError('Automation target database changed while reading');
}
return output;
}
function reviewAuthority(
command: Readonly<LocalReconciliationSecretConfigPlanCommand>,
terminal: Readonly<LocalReconciliationApplicationTerminal>,
@@ -390,7 +576,9 @@ function reviewAuthority(
),
);
if (opened === null) {
configurationError('manual-required SQLite topology cannot be adapted');
configurationError(
'manual-required SQLite topology cannot be adapted',
);
}
}
},
@@ -426,6 +614,7 @@ function publishPlan(
authority: Readonly<SecretConfigReviewAuthority>,
dependencies: LocalReconciliationSecretConfigPlanDependencies,
uid: number,
automationTarget?: DatabaseSync,
): Readonly<LocalReconciliationSecretConfigPlanReceipt> {
let descriptor: number | undefined;
let createdStage = false;
@@ -463,27 +652,31 @@ function publishPlan(
preparedHeadDigest: head.headDigest,
preparedAtMs: command.request.preparedAtMs,
});
const generated = withLocalReconciliationSealedDatabase(
authority.planTerminal.bundle,
'target',
uid,
dependencies,
(target) =>
withLocalReconciliationSealedDatabase(
authority.planTerminal.bundle,
'legacy',
uid,
dependencies,
(legacy) =>
writeLocalReconciliationSecretConfigPlan({
descriptor: descriptor!,
maxBytes: maxPlanBytes(terminal.plan.profile),
header,
legacy,
target,
}),
),
);
const generate = (target: DatabaseSync) =>
withLocalReconciliationSealedDatabase(
authority.planTerminal.bundle,
'legacy',
uid,
dependencies,
(legacy) =>
writeLocalReconciliationSecretConfigPlan({
descriptor: descriptor!,
maxBytes: maxPlanBytes(terminal.plan.profile),
header,
legacy,
target,
}),
);
const generated =
automationTarget === undefined
? withLocalReconciliationSealedDatabase(
authority.planTerminal.bundle,
'target',
uid,
dependencies,
generate,
)
: generate(automationTarget);
if (generated === null || generated === undefined) {
configurationError('manual-required SQLite topology cannot be planned');
}
@@ -577,7 +770,10 @@ function sealDirectory(directory: string, uid: number): void {
}
}
function sealTerminal(selected: Readonly<SecretConfigPaths>, uid: number): void {
function sealTerminal(
selected: Readonly<SecretConfigPaths>,
uid: number,
): void {
if (fs.readdirSync(selected.staging).length !== 0) {
configurationError('staging must be empty before terminal seal');
}
@@ -679,11 +875,17 @@ export async function planLocalReconciliationSecretConfig(
): Promise<Readonly<LocalReconciliationSecretConfigPlanResult>> {
const command = normalizeLocalReconciliationSecretConfigPlanCommand(value);
const identity = currentIdentity();
for (const [directory, label] of [
const authorityDirectories: readonly (readonly [string, string])[] = [
[command.options.deploymentRoot, 'deploymentRoot'],
[command.options.applicationRoot, 'applicationRoot'],
[command.options.secretConfigRoot, 'secretConfigRoot'],
] as const) {
...(command.schemaVersion === 2 && 'automationApplyRoot' in command.options
? ([
[command.options.automationApplyRoot, 'automationApplyRoot'],
] as const)
: []),
];
for (const [directory, label] of authorityDirectories) {
validatePrivateDirectory(directory, identity.uid, label);
}
const terminal = await readLocalReconciliationApplicationTerminal(
@@ -710,11 +912,7 @@ export async function planLocalReconciliationSecretConfig(
);
validateCatalog(selected, false);
const receipt = readReceipt(selected.receipt, identity.uid, [0o600, 0o400]);
validateTerminalBinding(
receipt,
terminal,
command.request.secretConfigId,
);
validateTerminalBinding(receipt, terminal, command.request.secretConfigId);
if (
receipt.applicationPlanDigest !==
command.request.expectedApplicationPlanDigest ||
@@ -730,7 +928,10 @@ export async function planLocalReconciliationSecretConfig(
identity.uid,
);
const existing = head.state === 'reconciliation_secret_config_planned';
if (!existing) validateApplicationBinding(command, terminal, head);
if (!existing) {
validateApplicationBinding(command, terminal, head);
await automationTargetAuthority(command, terminal, head, identity.uid);
}
if (
existing &&
head.sourceRecordDigest !== receipt.secretConfigPlanDigest
@@ -754,6 +955,12 @@ export async function planLocalReconciliationSecretConfig(
identity.uid,
);
validateApplicationBinding(command, terminal, head);
const automationTarget = await automationTargetAuthority(
command,
terminal,
head,
identity.uid,
);
const authority = reviewAuthority(
command,
terminal,
@@ -771,15 +978,26 @@ export async function planLocalReconciliationSecretConfig(
'secretConfigPlanStaging',
);
validateCatalog(selected, false);
const receipt = publishPlan(
selected,
command,
terminal,
head,
authority,
dependencies,
identity.uid,
);
const publish = (target?: DatabaseSync) =>
publishPlan(
selected,
command,
terminal,
head,
authority,
dependencies,
identity.uid,
target,
);
const receipt =
automationTarget === null
? publish()
: await withAutomationTargetDatabase(
automationTarget,
identity.uid,
dependencies,
publish,
);
dependencies.afterPlanPublished?.();
authority.confirmDecisionFileIdentity();
publishExactFile(
@@ -813,8 +1031,18 @@ export async function verifyLocalReconciliationSecretConfigPlan(
command.options.secretConfigRoot,
command.request.secretConfigId,
);
validateDirectory(selected.root, identity.uid, [0o500], 'Secret/Config plan root');
validateDirectory(selected.staging, identity.uid, [0o500], 'Secret/Config staging');
validateDirectory(
selected.root,
identity.uid,
[0o500],
'Secret/Config plan root',
);
validateDirectory(
selected.staging,
identity.uid,
[0o500],
'Secret/Config staging',
);
validateCatalog(selected, true);
const receipt = readReceipt(selected.receipt, identity.uid, [0o400]);
if (
@@ -887,6 +887,97 @@ function automationReadyDatabaseInitializer() {
};
}
function crossDomainReconciliationDatabaseInitializer() {
return ({ legacySourcePath, recoveryPath, targetDatabasePath }) => {
const legacy = new DatabaseSync(legacySourcePath);
legacy.exec(`
CREATE TABLE "Crontabs" (
id INTEGER PRIMARY KEY,
name TEXT,
command TEXT,
schedule TEXT,
saved INTEGER,
isSystem INTEGER,
isDisabled INTEGER,
isPinned INTEGER,
labels TEXT,
sub_id INTEGER,
extra_schedules TEXT,
task_before TEXT,
task_after TEXT,
log_name TEXT,
allow_multiple_instances INTEGER,
work_dir TEXT
);
CREATE TABLE "Envs" (
id INTEGER PRIMARY KEY,
name TEXT,
value TEXT,
status INTEGER,
position REAL,
"isPinned" INTEGER,
"createdAt" TEXT
);
CREATE TABLE "CrontabStats" (
id INTEGER PRIMARY KEY,
ref_id INTEGER NOT NULL,
date TEXT NOT NULL,
run_count INTEGER,
success_count INTEGER,
fail_count INTEGER,
total_time INTEGER,
max_time INTEGER,
"createdAt" TEXT NOT NULL,
"updatedAt" TEXT NOT NULL
);
CREATE TABLE "RunningInstances" (
id INTEGER PRIMARY KEY,
cron_id INTEGER NOT NULL,
run_id TEXT,
attempt_id TEXT,
pid INTEGER,
log_path TEXT,
started_at INTEGER NOT NULL,
finished_at INTEGER,
status INTEGER NOT NULL,
exit_code INTEGER,
"createdAt" TEXT NOT NULL,
"updatedAt" TEXT NOT NULL
);
INSERT INTO "Crontabs" (
id, name, command, schedule, saved, isSystem, isDisabled, isPinned
) VALUES (1, 'nightly', 'task nightly.js', '0 0 * * *', 1, 0, 0, 0);
INSERT INTO "Envs" (
id, name, value, status, position, "isPinned", "createdAt"
) VALUES (1, 'ACTIVE_TOKEN', 'private-secret-value', 0, 1, 0, '2026-01-01');
INSERT INTO "CrontabStats" (
id, ref_id, date, run_count, success_count, fail_count,
total_time, max_time, "createdAt", "updatedAt"
) VALUES (1, 1, '2026-01-01', 1, 1, 0, 100, 100, '2026-01-01', '2026-01-01');
`);
legacy.close();
fs.chmodSync(legacySourcePath, 0o600);
fs.copyFileSync(legacySourcePath, recoveryPath);
fs.chmodSync(recoveryPath, 0o600);
fs.copyFileSync(legacySourcePath, targetDatabasePath);
fs.chmodSync(targetDatabasePath, 0o600);
const migration = spawnSync(
process.execPath,
[
'-e',
`require('@qinglong/local-sqlite/migration')
.migrateLocalSqlitePath({ databasePath: process.argv[1], profile: 'edge' })
.catch((error) => { console.error(error); process.exitCode = 1; });`,
targetDatabasePath,
],
{ encoding: 'utf8', cwd: path.join(__dirname, '..') },
);
assert.equal(migration.status, 0, migration.stderr);
insertSecretConfigOwnerBinding(targetDatabasePath);
};
}
function insertSecretConfigOwnerBinding(targetDatabasePath) {
const target = new DatabaseSync(targetDatabasePath);
target.exec(`
@@ -1416,10 +1507,12 @@ async function plannedAutomationFixture(t, options = {}) {
createDefaultSidecars: false,
targetInsideDeploymentRoot: true,
initializeDatabases:
options.readyTarget === true
options.initializeDatabases ??
(options.readyTarget === true
? automationReadyDatabaseInitializer()
: automationDatabaseInitializer(),
: automationDatabaseInitializer()),
mutateTarget(paths) {
if (options.mutateTarget) return options.mutateTarget(paths);
return options.readyTarget === true
? mutateReadyAutomationTarget(paths)
: mutateAutomationTarget(paths, options.occupied === true);
@@ -1438,6 +1531,7 @@ async function plannedAutomationFixture(t, options = {}) {
options.occupied === true ? 'retain_both' : 'adopt_legacy';
selected.reason =
options.occupied === true ? 'preserve_both' : 'prefer_legacy';
options.mutateDecisions?.(records);
},
});
const preparedApplication = await prepareLocalReconciliationApplication(
@@ -1839,6 +1933,9 @@ async function appliedAutomationFixture(t, options = {}) {
automationId:
options.automationId ?? '00000000-0000-4000-8000-000000000484',
readyTarget: true,
initializeDatabases: options.initializeDatabases,
mutateTarget: options.mutateTarget,
mutateDecisions: options.mutateDecisions,
});
assert.equal(state.application.outcome, 'adapter_and_manual_required');
const decisionId =
@@ -4008,6 +4105,169 @@ test('Secret/Config plan keeps active Env and unknown Configs manual', async (t)
});
});
test('Secret/Config plan follows applied Automation and preserved Run History on an adopted legacy target', async (t) => {
const state = await appliedAutomationFixture(t, {
suffix: 'cross-domain-secret-config-plan',
planId: '00000000-0000-4000-8000-000000000425',
reviewId: '00000000-0000-4000-8000-000000000426',
applicationId: '00000000-0000-4000-8000-000000000427',
automationId: '00000000-0000-4000-8000-000000000428',
decisionId: '019b0000-0000-7000-8000-000000000425',
mutationId: '00000000-0000-4000-8000-000000000429',
initializeDatabases: crossDomainReconciliationDatabaseInitializer(),
mutateDecisions(records) {
let secretConfigCount = 0;
let runHistoryCount = 0;
for (const record of records) {
if (record.kind !== 'qinglong3-local-reconciliation-review-decision') {
continue;
}
if (record.domain === 'secret_and_config') {
record.disposition = 'manual_external';
record.reason = 'external_recovery_required';
secretConfigCount += 1;
} else if (
record.database === 'legacy' &&
record.domain === 'run_history'
) {
record.disposition = 'retain_both';
record.reason = 'preserve_both';
runHistoryCount += 1;
}
}
assert.ok(secretConfigCount > 0);
assert.ok(runHistoryCount > 0);
},
});
assert.equal(state.application.outcome, 'adapter_and_manual_required');
const runHistoryRoot = path.join(
path.dirname(state.captureRoot),
'cross-domain-run-history',
);
fs.mkdirSync(runHistoryRoot, { mode: 0o700 });
const preservationCommand = {
schemaVersion: 1,
operation: 'local.deployment.reconciliation.run-history.preserve',
options: {
deploymentRoot: state.deploymentRoot,
applicationRoot: state.applicationRoot,
runHistoryRoot,
allowRootService: rootAcknowledgement(),
},
request: {
preservationId: '00000000-0000-4000-8000-000000000430',
applicationId: state.application.applicationId,
expectedApplicationPlanDigest: state.application.applicationPlanDigest,
expectedHeadDigest: state.applied.instanceHeadDigest,
decisionFilePath: state.reviewFile.filePath,
preservedAtMs: state.applyCommand.request.appliedAtMs + 1,
},
};
const preserved = await preserveLocalReconciliationRunHistory(
preservationCommand,
);
assert.ok(preserved.legacyFactCount > 0);
assert.ok(preserved.targetFactCount > 0);
const preservationVerified = await verifyLocalReconciliationRunHistory({
schemaVersion: 1,
operation: 'local.deployment.reconciliation.run-history.verify',
options: preservationCommand.options,
request: {
preservationId: preservationCommand.request.preservationId,
applicationId: preservationCommand.request.applicationId,
expectedPreservationDigest: preserved.preservationDigest,
decisionFilePath: state.reviewFile.filePath,
},
});
assert.equal(preservationVerified.status, 'verified');
const secretConfigRoot = path.join(
path.dirname(state.captureRoot),
'cross-domain-secret-config-plan',
);
fs.mkdirSync(secretConfigRoot, { mode: 0o700 });
const secretConfigId = '00000000-0000-4000-8000-000000000431';
const secretConfigCommand = {
schemaVersion: 2,
operation: 'local.deployment.reconciliation.secret-config.plan',
options: {
deploymentRoot: state.deploymentRoot,
applicationRoot: state.applicationRoot,
secretConfigRoot,
automationApplyRoot: state.automationApplyRoot,
allowRootService: rootAcknowledgement(),
},
request: {
secretConfigId,
applicationId: state.application.applicationId,
expectedApplicationPlanDigest: state.application.applicationPlanDigest,
expectedHeadDigest: state.applied.instanceHeadDigest,
decisionFilePath: state.reviewFile.filePath,
projectId: 'default',
preparedAtMs: preservationCommand.request.preservedAtMs + 1,
automation: {
automationId: state.automationCommand.request.automationId,
decisionId: state.decisionId,
expectedApplyDigest: state.applied.applyDigest,
},
},
};
const legacyPlanCommand = {
...secretConfigCommand,
schemaVersion: 1,
options: {
deploymentRoot: state.deploymentRoot,
applicationRoot: state.applicationRoot,
secretConfigRoot,
allowRootService: rootAcknowledgement(),
},
request: {
...secretConfigCommand.request,
automation: undefined,
},
};
delete legacyPlanCommand.request.automation;
await assert.rejects(
planLocalReconciliationSecretConfig(legacyPlanCommand),
/Automation target authority is required/,
);
await assert.rejects(
planLocalReconciliationSecretConfig({
...secretConfigCommand,
request: {
...secretConfigCommand.request,
automation: {
...secretConfigCommand.request.automation,
expectedApplyDigest: 'f'.repeat(64),
},
},
}),
/Automation target authority is detached/,
);
const planned = await planLocalReconciliationSecretConfig(
secretConfigCommand,
);
assert.equal(planned.outcome, 'ready');
assert.equal(planned.eligibleBindingCount, 1);
assert.equal(planned.adoptedLegacyTaskCount, 1);
const verified = await verifyLocalReconciliationSecretConfigPlan({
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.verify',
options: {
deploymentRoot: state.deploymentRoot,
applicationRoot: state.applicationRoot,
secretConfigRoot,
allowRootService: rootAcknowledgement(),
},
request: {
secretConfigId,
expectedSecretConfigPlanDigest: planned.secretConfigPlanDigest,
},
});
assert.equal(verified.status, 'verified');
});
test('Secret/Config decision reauthenticates the same reviewer, seals exact candidates and verifies content-free', async (t) => {
const state = await plannedSecretConfigDecisionFixture(t, {
suffix: 'decision-terminal',
@@ -594,7 +594,7 @@ EOF
secret_prepared_ms=$((preserved_ms + 1))
cat >"$command_root/secret-config-plan.json" <<EOF
{"schemaVersion":1,"operation":"local.deployment.reconciliation.secret-config.plan","options":{"deploymentRoot":"$rehearsal_root","applicationRoot":"$application_root","secretConfigRoot":"$secret_config_root","allowRootService":$allow_root_service},"request":{"secretConfigId":"$SECRET_CONFIG_ID","applicationId":"$APPLICATION_ID","expectedApplicationPlanDigest":"$application_plan_digest","expectedHeadDigest":"$applied_head_digest","decisionFilePath":"$secondary_input","projectId":"default","preparedAtMs":$secret_prepared_ms}}
{"schemaVersion":2,"operation":"local.deployment.reconciliation.secret-config.plan","options":{"deploymentRoot":"$rehearsal_root","applicationRoot":"$application_root","secretConfigRoot":"$secret_config_root","automationApplyRoot":"$automation_apply_root","allowRootService":$allow_root_service},"request":{"secretConfigId":"$SECRET_CONFIG_ID","applicationId":"$APPLICATION_ID","expectedApplicationPlanDigest":"$application_plan_digest","expectedHeadDigest":"$applied_head_digest","decisionFilePath":"$secondary_input","projectId":"default","preparedAtMs":$secret_prepared_ms,"automation":{"automationId":"$AUTOMATION_ID","decisionId":"$AUTOMATION_DECISION_ID","expectedApplyDigest":"$apply_digest"}}}
EOF
chmod 0600 "$command_root/secret-config-plan.json"
phase 'materialize reviewed Secret/Config candidate plan'