feat(local): publish secret config application atomically

This commit is contained in:
whyour
2026-08-24 00:11:48 +08:00
parent e261d4130d
commit f822e92b02
25 changed files with 2149 additions and 58 deletions
@@ -25,7 +25,7 @@ test('authentication projection opens the target read-only without journal or fi
try {
assert.equal(database.profile, 'edge');
assert.equal(database.readiness.contractName, 'local-control-core');
assert.equal(database.readiness.contractVersion, 51);
assert.equal(database.readiness.contractVersion, 52);
assert.equal(await database.apiCredentials.resolve('absent'), null);
assert.equal(await database.ownerPepper.resolveKey('absent'), null);
} finally {
@@ -152,9 +152,11 @@ test('creates a reviewed edge database and opens runtime only after readiness',
'0100-capability-v50',
'0101-legacy-adoption-provenance',
'0102-capability-v51',
'0103-secret-config-applications',
'0104-capability-v52',
]);
assert.equal(migrated.readiness.contractName, 'local-control-core');
assert.equal(migrated.readiness.contractVersion, 51);
assert.equal(migrated.readiness.contractVersion, 52);
assert.equal(migrated.readiness.journalMode, 'delete');
assert.equal(fs.statSync(databasePath).mode & 0o777, 0o600);
@@ -600,8 +602,8 @@ test('backfills v14 execution revisions with a verified independent digest', asy
.get(),
},
{
contract_version: 51,
migration_id: '0101-legacy-adoption-provenance',
contract_version: 52,
migration_id: '0103-secret-config-applications',
},
);
} finally {
@@ -788,19 +790,19 @@ test('excludes reviewed optional feature tables while preserving unknown table d
const options = { databasePath, profile: 'edge' };
await migrateLocalSqlitePath(options);
const client = new DatabaseSync(databasePath);
assert.equal((await auditLocalSqlitePath(options)).tableCount, 85);
assert.equal((await auditLocalSqlitePath(options)).tableCount, 89);
client.exec(
'CREATE TABLE "ModelInvocationFeatureHead" (feature_id TEXT PRIMARY KEY)',
);
client.close();
assert.equal((await auditLocalSqlitePath(options)).tableCount, 85);
assert.equal((await auditLocalSqlitePath(options)).tableCount, 89);
const unknownClient = new DatabaseSync(databasePath);
unknownClient.exec('CREATE TABLE "UserExtensionData" (id TEXT PRIMARY KEY)');
unknownClient.close();
assert.equal((await auditLocalSqlitePath(options)).tableCount, 86);
assert.equal((await auditLocalSqlitePath(options)).tableCount, 90);
const triggerClient = new DatabaseSync(databasePath);
triggerClient.exec(`
@@ -156,7 +156,7 @@ test('atomically admits one generation-bound Workflow Run and exactly replays it
},
{ runs: 1, steps: 2, events: 3, mutations: 2, admissions: 1 },
);
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 51);
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 52);
});
test('runs an optional authorization guard inside new and replay transactions', async (t) => {
@@ -288,7 +288,7 @@ test('exactly replays immutable admission after the Workflow StepRun advances',
},
{ status: 'running', version: 5, eventSequence: 5 },
);
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 51);
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 52);
});
test('fails closed before writing when the exact installation is not active', async (t) => {
@@ -231,7 +231,7 @@ test('atomically admits the exact reconciled local Task revision and replays it'
stepAttemptCount: 0,
},
);
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 51);
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 52);
});
test('bounds candidate paging before SQL and fences cancellation', async (t) => {
@@ -40,9 +40,9 @@ test('creates and exactly replays a reviewed rollout backup', async (t) => {
await migrateLocalSqlitePath(state);
const prepared = await createLocalSqliteRolloutBackup(state);
assert.equal(prepared.status, 'prepared');
assert.equal(prepared.contractVersion, 51);
assert.equal(prepared.writeContractVersion, 51);
assert.equal(LOCAL_SQLITE_WRITE_CONTRACT_VERSION, 51);
assert.equal(prepared.contractVersion, 52);
assert.equal(prepared.writeContractVersion, 52);
assert.equal(LOCAL_SQLITE_WRITE_CONTRACT_VERSION, 52);
assert.match(prepared.sha256, /^[0-9a-f]{64}$/);
assert.equal(prepared.bytes > 0, true);
assert.equal(prepared.pageCount > 0, true);
@@ -0,0 +1,401 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const { migrateLocalSqlitePath } = require('../dist/migration/migration');
const {
openLocalSqliteAdoptionDatabase,
} = require('@qinglong/local-sqlite/adoption');
const {
LocalSecretConfigApplicationConflictError,
openLocalSqliteSecretConfigApplicationDatabase,
} = require('@qinglong/local-sqlite/secret-config-application');
const SUBJECT = Object.freeze({ type: 'user', id: 'local-owner' });
const FENCE = Object.freeze({ projectVersion: 1, bindingVersion: 1 });
const ADOPTION_MUTATION = '12345678-1234-4123-8123-123456789abc';
const APPLICATION_MUTATION = '87654321-4321-4123-8123-cba987654321';
const SECRET_MUTATION = 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee';
function digest(character) {
return character.repeat(64);
}
function fixture(t) {
const directory = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-secret-config-application-'),
);
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
return path.join(directory, 'qinglong3.sqlite');
}
async function preparedDatabase(t) {
const databasePath = fixture(t);
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const client = new DatabaseSync(databasePath);
client
.prepare(
`INSERT INTO "QingLong3ProjectRoleBindings" (
"project_id", "subject_type", "subject_id", "version", "state", "role",
"mutation_id", "changed_by_type", "changed_by_id", "created_at_ms"
) VALUES ('default', 'user', 'local-owner', 1, 'active', 'owner',
'secret-config-owner-binding', 'user', 'local-owner', 1)`,
)
.run();
client.close();
const adoption = await openLocalSqliteAdoptionDatabase({
databasePath,
profile: 'edge',
});
await adoption.publisher.publish({
mutationId: ADOPTION_MUTATION,
decisionId: '019f7200-0000-7000-8000-000000000001',
projectId: 'default',
profile: 'edge',
planDigest: digest('1'),
inventoryDigest: digest('2'),
decisionDigest: digest('3'),
receiptDigest: digest('4'),
authorizationFileDigest: digest('5'),
rowCount: 1,
skippedCount: 0,
subject: SUBJECT,
fence: FENCE,
audit: {
eventId: ADOPTION_MUTATION,
requestId: 'automation-adoption',
operationId: 'task.adopt',
projectId: 'default',
subject: SUBJECT,
authenticationId: 'local-console:review',
outcome: 'allowed',
reasons: ['project_role_allowed'],
fence: FENCE,
occurredAtMs: 100,
},
candidates: [
{
rowOrdinal: 1,
sourceDigest: digest('6'),
task: {
taskId: 'legacy-cron:1',
name: 'Legacy Task',
kind: 'command',
spec: {
schema: 'qinglong/command@v1',
config: {
command: { kind: 'argv', file: '/bin/echo', args: ['legacy'] },
},
},
labels: { source: 'legacy-adoption' },
enabled: true,
},
triggers: [
{
triggerId: 'legacy-cron:1:cron:1',
spec: {
schema: 'qinglong/cron@v1',
config: {
expression: '0 0 * * *',
timezone: 'UTC',
misfirePolicy: 'skip',
},
},
enabled: true,
},
],
},
],
confirmExternalAuthority() {},
createdAtMs: 100,
});
await adoption.close();
return databasePath;
}
function applicationCommand(confirmExternalAuthority = () => {}) {
return {
mutationId: APPLICATION_MUTATION,
projectId: 'default',
profile: 'edge',
secretConfigPlanDigest: digest('7'),
decisionDigest: digest('8'),
candidateSetDigest: digest('9'),
automationAdoptionSetDigest: digest('a'),
subject: SUBJECT,
fence: FENCE,
audit: {
eventId: APPLICATION_MUTATION,
requestId: 'secret-config-application',
operationId: 'secret-config.apply',
projectId: 'default',
subject: SUBJECT,
authenticationId: 'local-console:review',
outcome: 'allowed',
reasons: ['project_role_allowed'],
fence: FENCE,
occurredAtMs: 200,
},
secrets: [
{
ordinal: 1,
disposition: 'active_binding',
candidateDigest: digest('b'),
sourceSetDigest: digest('c'),
environmentName: 'LEGACY_TOKEN',
envelope: {
projectId: 'default',
name: 'legacy-db-env-bbbbbbbbbbbbbbbb',
version: 1,
mutationId: SECRET_MUTATION,
keyId: 'active-key',
algorithm: 'aes-256-gcm',
nonce: Buffer.alloc(12, 1).toString('base64url'),
ciphertext: Buffer.from('ciphertext').toString('base64url'),
authTag: Buffer.alloc(16, 2).toString('base64url'),
createdAtMs: 200,
},
audit: {
eventId: SECRET_MUTATION,
requestId: 'secret-config-application',
operationId: 'secret.create',
projectId: 'default',
subject: SUBJECT,
authenticationId: 'local-console:review',
outcome: 'allowed',
reasons: ['project_role_allowed'],
fence: FENCE,
occurredAtMs: 200,
},
},
],
appliedAtMs: 200,
confirmExternalAuthority,
};
}
test('atomically binds Secret, Task, dispatch, Trigger, schedule and replay ledger', async (t) => {
const databasePath = await preparedDatabase(t);
const database = await openLocalSqliteSecretConfigApplicationDatabase({
databasePath,
profile: 'edge',
});
const command = applicationCommand();
const inserted = await database.publisher.publish(command);
assert.equal(inserted.status, 'inserted');
assert.equal(inserted.application.receipt.activeBindingCount, 1);
assert.equal(inserted.application.receipt.taskCount, 1);
assert.equal(inserted.application.receipt.triggerCount, 1);
assert.equal((await database.publisher.publish(command)).status, 'existing');
await database.close();
const client = new DatabaseSync(databasePath, { readOnly: true });
assert.equal(
client
.prepare(
`SELECT "current_revision" AS revision FROM "QingLong3TaskDefinitions" WHERE "task_id" = 'legacy-cron:1'`,
)
.get().revision,
2,
);
assert.equal(
client
.prepare(
`SELECT "current_revision" AS revision FROM "QingLong3Triggers" WHERE "trigger_id" = 'legacy-cron:1:cron:1'`,
)
.get().revision,
2,
);
assert.equal(
client
.prepare(
`SELECT "trigger_revision" AS revision FROM "QingLong3LocalTriggerSchedules" WHERE "trigger_id" = 'legacy-cron:1:cron:1'`,
)
.get().revision,
2,
);
assert.equal(
client
.prepare(
`SELECT count(*) AS count FROM "QingLong3LocalSecretEnvelopes" WHERE "secret_name" = 'legacy-db-env-bbbbbbbbbbbbbbbb'`,
)
.get().count,
1,
);
assert.equal(
client
.prepare(
`SELECT count(*) AS count FROM "QingLong3LocalTaskExecutionRevisions" WHERE "task_id" = 'legacy-cron:1' AND "task_revision" LIKE 'qltd:v1:2:%'`,
)
.get().count,
1,
);
client.close();
});
test('rolls every database mutation back when the commit authority changes', async (t) => {
const databasePath = await preparedDatabase(t);
const database = await openLocalSqliteSecretConfigApplicationDatabase({
databasePath,
profile: 'edge',
});
let checks = 0;
await assert.rejects(
database.publisher.publish(
applicationCommand(() => {
checks += 1;
if (checks === 2) throw new Error('instance head drifted');
}),
),
/unavailable/,
);
await database.close();
const client = new DatabaseSync(databasePath, { readOnly: true });
assert.equal(
client
.prepare(
`SELECT count(*) AS count FROM "QingLong3SecretConfigApplications"`,
)
.get().count,
0,
);
assert.equal(
client
.prepare(
`SELECT "current_revision" AS revision FROM "QingLong3TaskDefinitions" WHERE "task_id" = 'legacy-cron:1'`,
)
.get().revision,
1,
);
assert.equal(
client
.prepare(
`SELECT count(*) AS count FROM "QingLong3LocalSecretEnvelopes" WHERE "secret_name" = 'legacy-db-env-bbbbbbbbbbbbbbbb'`,
)
.get().count,
0,
);
client.close();
});
test('fails closed on occupied Secret without advancing Task or Trigger heads', async (t) => {
const databasePath = await preparedDatabase(t);
const client = new DatabaseSync(databasePath);
client
.prepare(
`INSERT INTO "QingLong3LocalSecretEnvelopes" ("project_id", "secret_name", "version", "mutation_id", "key_id", "algorithm", "nonce", "ciphertext", "auth_tag", "created_at_ms") VALUES ('default', 'legacy-db-env-bbbbbbbbbbbbbbbb', 1, '11111111-2222-4333-8444-555555555555', 'other-key', 'aes-256-gcm', ?, ?, ?, 150)`,
)
.run(Buffer.alloc(12), Buffer.from('occupied'), Buffer.alloc(16));
client.close();
const database = await openLocalSqliteSecretConfigApplicationDatabase({
databasePath,
profile: 'edge',
});
await assert.rejects(
database.publisher.publish(applicationCommand()),
LocalSecretConfigApplicationConflictError,
);
await database.close();
const inspected = new DatabaseSync(databasePath, { readOnly: true });
assert.equal(
inspected
.prepare(
`SELECT "current_revision" AS revision FROM "QingLong3TaskDefinitions" WHERE "task_id" = 'legacy-cron:1'`,
)
.get().revision,
1,
);
assert.equal(
inspected
.prepare(
`SELECT count(*) AS count FROM "QingLong3SecretConfigApplications"`,
)
.get().count,
0,
);
inspected.close();
});
test('rejects incomplete Trigger provenance and rolls the streamed transaction back', async (t) => {
const databasePath = await preparedDatabase(t);
const client = new DatabaseSync(databasePath);
client.prepare(`DELETE FROM "QingLong3LegacyAdoptionTriggers"`).run();
client.close();
const database = await openLocalSqliteSecretConfigApplicationDatabase({
databasePath,
profile: 'edge',
});
await assert.rejects(
database.publisher.publish(applicationCommand()),
LocalSecretConfigApplicationConflictError,
);
await database.close();
const inspected = new DatabaseSync(databasePath, { readOnly: true });
assert.equal(
inspected
.prepare(
`SELECT "current_revision" AS revision FROM "QingLong3TaskDefinitions" WHERE "task_id" = 'legacy-cron:1'`,
)
.get().revision,
1,
);
assert.equal(
inspected
.prepare(
`SELECT count(*) AS count FROM "QingLong3SecretConfigApplicationTasks"`,
)
.get().count,
0,
);
inspected.close();
});
test('rejects replay after the durable Trigger schedule drifts', async (t) => {
const databasePath = await preparedDatabase(t);
const database = await openLocalSqliteSecretConfigApplicationDatabase({
databasePath,
profile: 'edge',
});
const command = applicationCommand();
await database.publisher.publish(command);
await database.close();
const client = new DatabaseSync(databasePath);
client
.prepare(
`UPDATE "QingLong3LocalTriggerSchedules" SET "trigger_revision" = 1 WHERE "trigger_id" = 'legacy-cron:1:cron:1'`,
)
.run();
client.close();
const reopened = await openLocalSqliteSecretConfigApplicationDatabase({
databasePath,
profile: 'edge',
});
await assert.rejects(
reopened.publisher.publish(command),
LocalSecretConfigApplicationConflictError,
);
await reopened.close();
});
test('rejects empty applications and Secret timestamps outside the application instant', async (t) => {
const databasePath = await preparedDatabase(t);
const database = await openLocalSqliteSecretConfigApplicationDatabase({
databasePath,
profile: 'edge',
});
assert.throws(
() => database.publisher.publish({ ...applicationCommand(), secrets: [] }),
LocalSecretConfigApplicationConflictError,
);
const command = applicationCommand();
command.secrets[0].envelope.createdAtMs = command.appliedAtMs - 1;
assert.throws(
() => database.publisher.publish(command),
LocalSecretConfigApplicationConflictError,
);
await database.close();
});