feat(local): publish secret config reconciliation plans

This commit is contained in:
whyour
2026-08-23 17:27:01 +08:00
parent fbb67ff5c8
commit 5dd8678d7e
13 changed files with 1811 additions and 20 deletions
@@ -22,6 +22,7 @@ const {
readLocalReconciliationAutomationDecisionTerminal,
rollbackLocalReconciliationAutomationApply,
planLocalReconciliationAutomation,
planLocalReconciliationSecretConfig,
prepareLocalReconciliationPlan,
prepareLocalReconciliationReview,
verifyLocalReconciliationCapture,
@@ -29,6 +30,7 @@ const {
verifyLocalReconciliationAutomationDecision,
verifyLocalReconciliationAutomationApply,
verifyLocalReconciliationAutomationPlan,
verifyLocalReconciliationSecretConfigPlan,
verifyLocalReconciliationCompletion,
verifyLocalReconciliationPlan,
verifyLocalReconciliationReview,
@@ -791,6 +793,58 @@ function automationReadyDatabaseInitializer() {
};
}
function secretConfigDatabaseInitializer({
active = false,
configs = false,
} = {}) {
return ({ legacySourcePath, recoveryPath, targetDatabasePath }) => {
const legacy = new DatabaseSync(legacySourcePath);
legacy.exec(`
CREATE TABLE "Envs" (
id INTEGER PRIMARY KEY,
name TEXT,
value TEXT,
status INTEGER,
position REAL,
"isPinned" INTEGER,
"createdAt" TEXT
);
INSERT INTO "Envs" VALUES (
1,
'${active ? 'ACTIVE_TOKEN' : 'DISABLED_TOKEN'}',
'private-secret-value',
${active ? 0 : 1},
1,
0,
'2026-01-01'
);
${
configs
? 'CREATE TABLE "Configs" (id INTEGER PRIMARY KEY, value TEXT); INSERT INTO "Configs" VALUES (1, \'private-config-value\');'
: ''
}
`);
legacy.close();
fs.chmodSync(legacySourcePath, 0o600);
fs.copyFileSync(legacySourcePath, recoveryPath);
fs.chmodSync(recoveryPath, 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);
fs.chmodSync(targetDatabasePath, 0o600);
};
}
function mutateAutomationTarget({ targetDatabasePath }, occupied = false) {
const target = new DatabaseSync(targetDatabasePath);
if (occupied) {
@@ -1167,6 +1221,69 @@ function applicationCommitCommand(state, prepared) {
};
}
async function secretConfigPlanFixture(t, options = {}) {
const suffix = options.suffix ?? 'plan';
const state = await reviewedApplicationFixture(t, {
planId:
options.planId ?? '00000000-0000-4000-8000-000000000421',
reviewId:
options.reviewId ?? '00000000-0000-4000-8000-000000000422',
applicationId:
options.applicationId ?? '00000000-0000-4000-8000-000000000423',
reviewSuffix: `secret-config-${suffix}`,
createDefaultSidecars: false,
initializeDatabases: secretConfigDatabaseInitializer({
active: options.active === true,
configs: options.configs === true,
}),
mutateTarget({ targetDatabasePath }) {
const target = new DatabaseSync(targetDatabasePath);
target.exec('PRAGMA user_version=1');
target.close();
return Object.freeze({});
},
});
const preparedApplication = await prepareLocalReconciliationApplication(
state.prepareApplicationCommand,
);
const application = await commitLocalReconciliationApplication(
applicationCommitCommand(state, preparedApplication),
);
const secretConfigRoot = path.join(
path.dirname(state.captureRoot),
`secret-config-plan-${suffix}`,
);
fs.mkdirSync(secretConfigRoot, { mode: 0o700 });
const secretConfigId =
options.secretConfigId ?? '00000000-0000-4000-8000-000000000424';
const command = {
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.plan',
options: {
deploymentRoot: state.deploymentRoot,
applicationRoot: state.applicationRoot,
secretConfigRoot,
allowRootService: rootAcknowledgement(),
},
request: {
secretConfigId,
applicationId: application.applicationId,
expectedApplicationPlanDigest: application.applicationPlanDigest,
expectedHeadDigest: application.instanceHeadDigest,
decisionFilePath: state.reviewFile.filePath,
projectId: 'default',
preparedAtMs: state.prepareApplicationCommand.request.preparedAtMs + 2,
},
};
return {
...state,
application,
secretConfigRoot,
secretConfigId,
secretConfigCommand: command,
};
}
async function plannedAutomationFixture(t, options = {}) {
const suffix = options.suffix ?? 'decision';
const state = await reviewedApplicationFixture(t, {
@@ -3197,6 +3314,186 @@ test('completion fence seals all eight no-effect domains before target restart',
assert.equal(activeHead.generation, 2);
});
test('Secret/Config plan publishes, seals, verifies and stays content-free', async (t) => {
const state = await secretConfigPlanFixture(t, { suffix: 'terminal' });
const opened = [];
const closed = [];
const planned = await planLocalReconciliationSecretConfig(
state.secretConfigCommand,
{
beforeDatabaseOpen(kind, mode, cacheKiB) {
opened.push({ kind, mode, cacheKiB });
},
afterDatabaseClose(kind) {
closed.push(kind);
},
},
);
assert.equal(planned.status, 'prepared');
assert.equal(planned.state, 'reconciliation_secret_config_planned');
assert.equal(planned.outcome, 'ready');
assert.equal(planned.rowCount, 1);
assert.equal(planned.eligibleBindingCount, 0);
assert.equal(planned.eligiblePreservationCount, 1);
assert.equal(planned.adoptedLegacyTaskCount, 0);
assert.deepEqual(opened, [
{ kind: 'legacy', mode: 'main_only_immutable', cacheKiB: 2048 },
{ kind: 'target', mode: 'main_only_immutable', cacheKiB: 2048 },
{ kind: 'target', mode: 'main_only_immutable', cacheKiB: 2048 },
{ kind: 'legacy', mode: 'main_only_immutable', cacheKiB: 2048 },
]);
assert.deepEqual(closed, ['legacy', 'target', 'legacy', 'target']);
const root = path.join(state.secretConfigRoot, state.secretConfigId);
assert.deepEqual(fs.readdirSync(root).sort(), [
'plan.ndjson',
'receipt.json',
'staging',
]);
assert.equal(fs.statSync(root).mode & 0o777, 0o500);
assert.equal(fs.statSync(path.join(root, 'staging')).mode & 0o777, 0o500);
assert.equal(fs.statSync(path.join(root, 'plan.ndjson')).mode & 0o777, 0o400);
assert.equal(fs.statSync(path.join(root, 'receipt.json')).mode & 0o777, 0o400);
const serialized = fs.readFileSync(path.join(root, 'plan.ndjson'), 'utf8');
for (const privateValue of [
'DISABLED_TOKEN',
'private-secret-value',
state.targetDatabasePath,
state.reviewFile.filePath,
]) {
assert.equal(serialized.includes(privateValue), false);
}
const receipt = JSON.parse(
fs.readFileSync(path.join(root, 'receipt.json'), 'utf8'),
);
assert.equal(receipt.unadaptedLegacyConfigCount, 0);
assert.match(receipt.automationAdoptionSetDigest, /^[0-9a-f]{64}$/);
const head = readLocalCutoverInstanceHead(
state.deploymentRoot,
state.captureCommand.request.instanceId,
state.uid,
);
assert.equal(head.state, 'reconciliation_secret_config_planned');
assert.equal(head.sourceRecordDigest, planned.secretConfigPlanDigest);
const verifyCommand = {
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.verify',
options: state.secretConfigCommand.options,
request: {
secretConfigId: state.secretConfigId,
expectedSecretConfigPlanDigest: planned.secretConfigPlanDigest,
},
};
const verified = await verifyLocalReconciliationSecretConfigPlan(
verifyCommand,
);
assert.equal(verified.status, 'verified');
assert.equal(
(await planLocalReconciliationSecretConfig(state.secretConfigCommand))
.status,
'existing',
);
const commandPath = path.join(
state.deploymentRoot,
'secret-config-verify-command.json',
);
fs.writeFileSync(commandPath, `${JSON.stringify(verifyCommand)}\n`, {
mode: 0o600,
});
const cli = spawnSync(
process.execPath,
[
path.join(__dirname, '../dist/deployment/localDeploymentCli.js'),
'reconciliation-secret-config-verify',
'--command-file',
commandPath,
],
{ encoding: 'utf8' },
);
assert.equal(cli.status, 0, cli.stderr);
assert.equal(JSON.parse(cli.stdout).status, 'verified');
assert.equal(cli.stdout.includes(state.secretConfigRoot), false);
assert.equal(cli.stdout.includes('DISABLED_TOKEN'), false);
assert.equal(cli.stderr, '');
});
test('Secret/Config plan recovers exact publication response loss windows', async (t) => {
for (const [hook, finalStatus] of [
['afterPlanPublished', 'prepared'],
['afterReceiptPublished', 'prepared'],
['afterTerminalSealed', 'prepared'],
['afterHeadAdvanced', 'existing'],
]) {
await t.test(hook, async (subtest) => {
const state = await secretConfigPlanFixture(subtest, { suffix: hook });
let fault = true;
await assert.rejects(
planLocalReconciliationSecretConfig(state.secretConfigCommand, {
[hook]() {
if (fault) {
fault = false;
throw new Error(`secret-config-${hook}-fault`);
}
},
}),
new RegExp(`secret-config-${hook}-fault`),
);
const recovered = await planLocalReconciliationSecretConfig(
state.secretConfigCommand,
);
assert.equal(recovered.status, finalStatus);
assert.equal(recovered.outcome, 'ready');
const verified = await verifyLocalReconciliationSecretConfigPlan({
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.verify',
options: state.secretConfigCommand.options,
request: {
secretConfigId: state.secretConfigId,
expectedSecretConfigPlanDigest: recovered.secretConfigPlanDigest,
},
});
assert.equal(verified.status, 'verified');
});
}
});
test('Secret/Config plan keeps active Env and unknown Configs manual', async (t) => {
await t.test('active without adopted task', async (subtest) => {
const state = await secretConfigPlanFixture(subtest, {
suffix: 'active-manual',
active: true,
});
const planned = await planLocalReconciliationSecretConfig(
state.secretConfigCommand,
);
assert.equal(planned.outcome, 'manual_required');
assert.equal(planned.eligibleBindingCount, 1);
assert.equal(planned.adoptedLegacyTaskCount, 0);
});
await t.test('historical Configs', async (subtest) => {
const state = await secretConfigPlanFixture(subtest, {
suffix: 'configs-manual',
configs: true,
});
const planned = await planLocalReconciliationSecretConfig(
state.secretConfigCommand,
);
assert.equal(planned.outcome, 'manual_required');
assert.equal(planned.unadaptedLegacyConfigCount, 1);
const serialized = fs.readFileSync(
path.join(
state.secretConfigRoot,
state.secretConfigId,
'plan.ndjson',
),
'utf8',
);
assert.equal(serialized.includes('private-config-value'), false);
});
});
test('completion fence retains automation rollback backup while other domains remain manual', async (t) => {
const state = await appliedAutomationFixture(t, {
suffix: 'completion-fence',
@@ -27,7 +27,8 @@ const HEADER = Object.freeze({
bundleFingerprintDigest: '1'.repeat(64),
profile: 'edge',
projectId: 'project-1',
tableDisposition: 'adopt_legacy',
tableDisposition: 'manual_external',
unadaptedLegacyConfigCount: 0,
preparedHeadDigest: '2'.repeat(64),
preparedAtMs: 1_780_000_000_000,
});
@@ -60,11 +61,60 @@ function databases() {
created_at_ms INTEGER NOT NULL,
PRIMARY KEY (project_id, secret_name, version)
);
CREATE TABLE "QingLong3LegacyAdoptions" (
mutation_id TEXT PRIMARY KEY,
decision_id TEXT NOT NULL,
project_id TEXT NOT NULL,
plan_digest TEXT NOT NULL,
inventory_digest TEXT NOT NULL,
decision_digest TEXT NOT NULL,
receipt_digest TEXT NOT NULL,
authorization_file_digest TEXT NOT NULL,
publication_digest TEXT NOT NULL,
row_count INTEGER NOT NULL,
adopted_task_count INTEGER NOT NULL,
adopted_trigger_count INTEGER NOT NULL,
skipped_count INTEGER NOT NULL,
audit_event_id TEXT NOT NULL,
created_at_ms INTEGER NOT NULL
);
`);
return { legacy, target };
}
function writePlan(t, legacy, target, maxBytes = 8 * 1024 * 1024) {
function insertAutomationAdoption(target, adoptedTaskCount = 1) {
const mutationId = '30000000-0000-4000-8000-000000000003';
target
.prepare(
`INSERT INTO "QingLong3LegacyAdoptions" VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
)
.run(
mutationId,
'019b0000-0000-7000-8000-000000000001',
HEADER.projectId,
'3'.repeat(64),
'4'.repeat(64),
'5'.repeat(64),
'6'.repeat(64),
'7'.repeat(64),
'8'.repeat(64),
adoptedTaskCount,
adoptedTaskCount,
0,
0,
mutationId,
HEADER.preparedAtMs,
);
}
function writePlan(
t,
legacy,
target,
maxBytes = 8 * 1024 * 1024,
header = HEADER,
) {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-secret-plan-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const filePath = path.join(directory, 'plan.ndjson');
@@ -74,7 +124,7 @@ function writePlan(t, legacy, target, maxBytes = 8 * 1024 * 1024) {
result = writeLocalReconciliationSecretConfigPlan({
descriptor,
maxBytes,
header: HEADER,
header,
legacy,
target,
});
@@ -107,6 +157,7 @@ test('writes a content-free Env plan with separate active and disabled candidate
(2, 'TOKEN', 'pinned-secret', 0, 1, 1, '2026-01-02'),
(3, 'DISABLED_TOKEN', 'disabled-secret', 1, 0, 0, '2026-01-03');
`);
insertAutomationAdoption(target);
const { result, records, serialized } = writePlan(t, legacy, target);
assert.equal(result.footer.outcome, 'ready');
@@ -114,6 +165,9 @@ test('writes a content-free Env plan with separate active and disabled candidate
assert.equal(result.footer.eligibleBindingCount, 1);
assert.equal(result.footer.eligiblePreservationCount, 1);
assert.equal(result.footer.targetConflictCount, 0);
assert.equal(result.footer.automationAdoptionRecordCount, 1);
assert.equal(result.footer.adoptedLegacyTaskCount, 1);
assert.match(result.footer.automationAdoptionSetDigest, /^[0-9a-f]{64}$/);
const candidates = records.filter((record) =>
record.kind.endsWith('-candidate'),
);
@@ -234,6 +288,28 @@ test('makes absent Envs no-effect and malformed Env manual', (t) => {
assert.equal(manual.serialized.includes('private-value'), false);
});
test('keeps active Env and historical Configs manual without adoption authority', (t) => {
const { legacy, target } = databases();
t.after(() => legacy.close());
t.after(() => target.close());
legacy.exec(
`INSERT INTO "Envs" VALUES
(1, 'TOKEN', 'private-value', 0, 1, 0, '2026-01-01')`,
);
const withoutAdoption = writePlan(t, legacy, target);
assert.equal(withoutAdoption.result.footer.outcome, 'manual_required');
assert.equal(withoutAdoption.result.footer.adoptedLegacyTaskCount, 0);
assert.equal(withoutAdoption.serialized.includes('private-value'), false);
insertAutomationAdoption(target);
const withConfigs = writePlan(t, legacy, target, 8 * 1024 * 1024, {
...HEADER,
unadaptedLegacyConfigCount: 1,
});
assert.equal(withConfigs.result.footer.outcome, 'manual_required');
assert.equal(withConfigs.result.footer.unadaptedLegacyConfigCount, 1);
});
test('fails closed before exceeding the plan byte budget', (t) => {
const { legacy, target } = databases();
t.after(() => legacy.close());