feat(ql3): gate local secret transitions before activation

This commit is contained in:
whyour
2026-08-13 20:29:47 +08:00
parent 326783ff14
commit c877147d0f
37 changed files with 2495 additions and 39 deletions
@@ -146,9 +146,11 @@ test('creates a reviewed edge database and opens runtime only after readiness',
'0094-capability-v47',
'0095-plugin-package-secret-binding-target-guard',
'0096-capability-v48',
'0097-plugin-package-secret-binding-transition-receipts',
'0098-capability-v49',
]);
assert.equal(migrated.readiness.contractName, 'local-control-core');
assert.equal(migrated.readiness.contractVersion, 48);
assert.equal(migrated.readiness.contractVersion, 49);
assert.equal(migrated.readiness.journalMode, 'delete');
assert.equal(fs.statSync(databasePath).mode & 0o777, 0o600);
@@ -594,8 +596,8 @@ test('backfills v14 execution revisions with a verified independent digest', asy
.get(),
},
{
contract_version: 48,
migration_id: '0095-plugin-package-secret-binding-target-guard',
contract_version: 49,
migration_id: '0097-plugin-package-secret-binding-transition-receipts',
},
);
} finally {
@@ -782,19 +784,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, 80);
assert.equal((await auditLocalSqlitePath(options)).tableCount, 81);
client.exec(
'CREATE TABLE "ModelInvocationFeatureHead" (feature_id TEXT PRIMARY KEY)',
);
client.close();
assert.equal((await auditLocalSqlitePath(options)).tableCount, 80);
assert.equal((await auditLocalSqlitePath(options)).tableCount, 81);
const unknownClient = new DatabaseSync(databasePath);
unknownClient.exec('CREATE TABLE "UserExtensionData" (id TEXT PRIMARY KEY)');
unknownClient.close();
assert.equal((await auditLocalSqlitePath(options)).tableCount, 81);
assert.equal((await auditLocalSqlitePath(options)).tableCount, 82);
const triggerClient = new DatabaseSync(databasePath);
triggerClient.exec(`
@@ -11,9 +11,18 @@ const {
createPluginPackageResourceGeneration,
} = require('@qinglong/runtime-core/plugin-package-resource-generation');
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
const {
createPluginPackageSecretBindingTransitionPlan,
} = require('@qinglong/runtime-core/plugin-package-secret-binding-transition-plan');
const {
createPluginPackageSecretBindingTransitionReceipt,
} = require('@qinglong/runtime-core/plugin-package-secret-binding-transition-receipt');
const {
LocalSqlitePluginPackageSecretBindingRepository,
} = require('../dist/plugin-package/secret-binding/repository');
const {
LocalSqlitePluginPackageSecretBindingTransitionReceiptRepository,
} = require('../dist/plugin-package/secret-binding/transitionReceiptRepository');
const { migrateLocalSqliteDatabase } = require('../dist/migration/migration');
const LOCK_DIGEST = 'a'.repeat(64);
@@ -247,7 +256,10 @@ test('rejects inactive targets and conflicting content', async (t) => {
test('publishes a reviewed current staged generation but rejects post-stage states', async (t) => {
const staged = await harness('staged');
t.after(() => staged.client.close());
assert.equal((await staged.repository.publish(staged.binding)).status, 'created');
assert.equal(
(await staged.repository.publish(staged.binding)).status,
'created',
);
for (const state of ['queued', 'activating']) {
const rejected = await harness(state);
@@ -259,6 +271,102 @@ test('publishes a reviewed current staged generation but rejects post-stage stat
}
});
test('persists one exact staged transition receipt and rejects durable drift', async (t) => {
const value = await harness('staged');
t.after(() => value.client.close());
const previousGeneration = createPluginPackageResourceGeneration({
installationId: 'install-1',
projectId: 'project-1',
packageName: 'example-monitor',
lockDigest: 'f'.repeat(64),
generation: 1,
previousActiveLockDigest: null,
contentDigest: '9'.repeat(64),
contents: MANIFEST.spec.contents,
});
const previousBinding = createPluginPackageSecretBinding({
generation: previousGeneration,
manifest: MANIFEST,
assignments: value.binding.entries.map(({ name, secretRef }) => ({
name,
secretRef,
})),
authority: value.binding.authority,
boundAtMs: 90,
});
value.client
.prepare(
`UPDATE "QingLong3PluginPackageInstalls"
SET lock_json = json_set(lock_json, '$.manifestDigest', ?)
WHERE installation_id = 'install-1'`,
)
.run(previousBinding.target.manifestDigest);
value.client
.prepare(
`UPDATE "QingLong3PluginPackageInstalls"
SET lock_json = json_set(lock_json, '$.manifestDigest', ?)
WHERE installation_id = 'install-2'`,
)
.run(value.binding.target.manifestDigest);
value.client
.prepare(
`UPDATE "QingLong3PluginPackageInstallHeads"
SET installation_id = 'install-1'
WHERE project_id = 'project-1' AND package_name = 'example-monitor'`,
)
.run();
await value.repository.publish(previousBinding);
value.client
.prepare(
`UPDATE "QingLong3PluginPackageInstallHeads"
SET installation_id = 'install-2'
WHERE project_id = 'project-1' AND package_name = 'example-monitor'`,
)
.run();
await value.repository.publish(value.binding);
const plan = createPluginPackageSecretBindingTransitionPlan({
previousTarget: previousBinding.target,
previousBinding,
previousAttemptGeneration: 1,
nextGeneration: value.generation,
nextManifest: MANIFEST,
assignments: value.binding.entries.map(({ name, secretRef }) => ({
name,
secretRef,
})),
plannedAtMs: 95,
});
const receipt = createPluginPackageSecretBindingTransitionReceipt({
transitionPlan: plan,
authority: value.binding.authority,
binding: value.binding,
committedAtMs: value.binding.boundAtMs,
});
const receipts =
new LocalSqlitePluginPackageSecretBindingTransitionReceiptRepository(
value.client,
);
assert.equal(receipts.publishInTransaction(receipt).status, 'created');
assert.equal(receipts.publishInTransaction(receipt).status, 'existing');
assert.deepEqual(
await receipts.find(value.generation.generationDigest),
receipt,
);
value.client.exec('PRAGMA ignore_check_constraints = ON');
value.client
.prepare(
`UPDATE "QingLong3PluginPackageSecretBindingTransitionReceipts"
SET receipt_json = json_set(receipt_json, '$.committedAtMs', 999)
WHERE generation_digest = ?`,
)
.run(value.generation.generationDigest);
await assert.rejects(
receipts.find(value.generation.generationDigest),
PluginPackageSecretBindingUnavailableError,
);
});
test('fails closed when durable binding JSON is changed in place', async (t) => {
const value = await harness();
t.after(() => value.client.close());
@@ -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, 48);
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 49);
});
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, 48);
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 49);
});
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, 48);
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 49);
});
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, 48);
assert.equal(prepared.writeContractVersion, 48);
assert.equal(LOCAL_SQLITE_WRITE_CONTRACT_VERSION, 48);
assert.equal(prepared.contractVersion, 49);
assert.equal(prepared.writeContractVersion, 49);
assert.equal(LOCAL_SQLITE_WRITE_CONTRACT_VERSION, 49);
assert.match(prepared.sha256, /^[0-9a-f]{64}$/);
assert.equal(prepared.bytes > 0, true);
assert.equal(prepared.pageCount > 0, true);