feat(ql3): enforce staged secret binding persistence

This commit is contained in:
whyour
2026-08-13 19:20:39 +08:00
parent f111d8b10c
commit 326783ff14
32 changed files with 1071 additions and 71 deletions
@@ -104,6 +104,8 @@ import { local0091PluginPackageSecretBindingsMigration } from '../migrations/009
import { local0092CapabilityV46Migration } from '../migrations/0092-capability-v46';
import { local0093PluginPackageSecretMaterializationGuardMigration } from '../migrations/0093-plugin-package-secret-materialization-guard';
import { local0094CapabilityV47Migration } from '../migrations/0094-capability-v47';
import { local0095PluginPackageSecretBindingTargetGuardMigration } from '../migrations/0095-plugin-package-secret-binding-target-guard';
import { local0096CapabilityV48Migration } from '../migrations/0096-capability-v48';
import type { LocalSqliteMigrationContext } from '../migrations/sqlMigration';
import {
LOCAL_SQLITE_MIGRATION_STREAM_ID,
@@ -220,6 +222,8 @@ export const localSqliteMigrationDefinition: MigrationStreamDefinition<LocalSqli
local0092CapabilityV46Migration,
local0093PluginPackageSecretMaterializationGuardMigration,
local0094CapabilityV47Migration,
local0095PluginPackageSecretBindingTargetGuardMigration,
local0096CapabilityV48Migration,
]),
});
@@ -482,5 +482,15 @@ export const localSqliteMigrationManifest: MigrationStreamManifest =
checksum:
'6ecbbef0b9d9b3c738cc47a80868e0871c47916fb67e7c37f8620f9099de735e',
}),
Object.freeze({
id: '0095-plugin-package-secret-binding-target-guard',
checksum:
'dc0f4051663fef5304f462896aab9a63a259d12933a2d33873cb7397097d4b4f',
}),
Object.freeze({
id: '0096-capability-v48',
checksum:
'07118f8e2f1e4f4aa7b9bb95ba9b70276f62de0e63d63cf5dcceacd3532853d9',
}),
]),
});
@@ -0,0 +1,8 @@
import { defineLocalSqliteMigration } from './sqlMigration';
import { LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGER_SQL } from '../plugin-package/secret-binding/pluginPackageSecretBindingTargetSchemaContract';
export const local0095PluginPackageSecretBindingTargetGuardMigration =
defineLocalSqliteMigration({
id: '0095-plugin-package-secret-binding-target-guard',
statements: [LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGER_SQL],
});
@@ -0,0 +1,14 @@
import { CAPABILITIES_V47 } from './0094-capability-v47';
import { defineLocalSqliteMigration } from './sqlMigration';
export const CAPABILITIES_V48 = CAPABILITIES_V47.replace(
'"plugin_package_secret_binding":1,',
'"plugin_package_secret_binding":1,"plugin_package_secret_binding_transition":1,',
);
export const local0096CapabilityV48Migration = defineLocalSqliteMigration({
id: '0096-capability-v48',
statements: [
`UPDATE "QingLong3SchemaCapabilities" SET contract_version = 48, migration_id = '0095-plugin-package-secret-binding-target-guard', capabilities = '${CAPABILITIES_V48}', updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER) WHERE contract_name = 'local-control-core' AND contract_version = 47 AND migration_id = '0093-plugin-package-secret-materialization-guard' AND capabilities = '${CAPABILITIES_V47}'`,
],
});
@@ -0,0 +1,53 @@
export const LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGER_NAME =
'ql3_plugin_package_secret_binding_target_guard' as const;
export const LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGER_SQL = `
CREATE TRIGGER ${LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGER_NAME}
BEFORE INSERT ON "QingLong3PluginPackageSecretBindings"
FOR EACH ROW
BEGIN
SELECT CASE WHEN NOT EXISTS (
SELECT 1
FROM "QingLong3PluginPackageInstallHeads" AS head
JOIN "QingLong3PluginPackageInstalls" AS install
ON install.installation_id = head.installation_id
AND install.project_id = head.project_id
AND install.package_name = head.package_name
WHERE head.project_id = NEW.project_id
AND head.package_name = NEW.package_name
AND install.installation_id = NEW.installation_id
AND install.lock_digest = NEW.lock_digest
AND install.target_generation = NEW.generation
AND json_extract(install.lock_json, '$.manifestDigest') =
NEW.manifest_digest
AND (
(
install.state = 'active' AND
install.active_lock_digest = install.lock_digest
) OR (
install.state = 'staged' AND
install.previous_active_lock_digest IS NOT NULL AND
install.active_lock_digest = install.previous_active_lock_digest AND
install.target_generation = (
SELECT MAX(history.target_generation)
FROM "QingLong3PluginPackageInstalls" AS history
WHERE history.project_id = install.project_id
AND history.package_name = install.package_name
) AND
EXISTS (
SELECT 1
FROM "QingLong3PluginPackageInstalls" AS previous
WHERE previous.project_id = install.project_id
AND previous.package_name = install.package_name
AND previous.lock_digest = install.previous_active_lock_digest
AND previous.state = 'active'
AND previous.active_lock_digest = previous.lock_digest
AND previous.target_generation < install.target_generation
)
)
)
) THEN RAISE(ABORT,
'Plugin Package Secret binding target is not current active or reviewed staged generation')
END;
END
`.trim();
@@ -220,10 +220,31 @@ export class LocalSqlitePluginPackageSecretBindingRepository
AND install.project_id = ?
AND install.package_name = ?
AND install.lock_digest = ?
AND install.active_lock_digest = ?
AND install.target_generation = ?
AND install.state = 'active'
AND json_extract(install.lock_json, '$.manifestDigest') = ?
AND (
(install.state = 'active' AND
install.active_lock_digest = install.lock_digest) OR
(install.state = 'staged' AND
install.previous_active_lock_digest IS NOT NULL AND
install.active_lock_digest = install.previous_active_lock_digest AND
install.target_generation = (
SELECT MAX(history.target_generation)
FROM "QingLong3PluginPackageInstalls" AS history
WHERE history.project_id = install.project_id
AND history.package_name = install.package_name
) AND
EXISTS (
SELECT 1
FROM "QingLong3PluginPackageInstalls" AS previous
WHERE previous.project_id = install.project_id
AND previous.package_name = install.package_name
AND previous.lock_digest = install.previous_active_lock_digest
AND previous.state = 'active'
AND previous.active_lock_digest = previous.lock_digest
AND previous.target_generation < install.target_generation
))
)
ON CONFLICT (generation_digest) DO NOTHING`,
)
.run(
@@ -243,14 +264,13 @@ export class LocalSqlitePluginPackageSecretBindingRepository
binding.target.projectId,
binding.target.packageName,
binding.target.lockDigest,
binding.target.lockDigest,
binding.target.generation,
binding.target.manifestDigest,
);
const stored = this.findStored(binding.target.generationDigest);
if (!stored) {
throw new PluginPackageSecretBindingConflictError(
'binding target is not the current active Package generation',
'binding target is not the current active or reviewed staged Package generation',
);
}
if (JSON.stringify(stored) !== bindingJson) {
@@ -3,13 +3,22 @@ import type { DatabaseSync } from 'node:sqlite';
import { localSqliteMigrationManifest } from '../migration/migrationManifest';
import { LocalSqliteMigrationStreamStore } from '../migration/migrationStreamStore';
import { LOCAL_PLUGIN_PACKAGE_SECRET_MATERIALIZATION_TRIGGER_SQL } from '../plugin-package/pluginPackageSecretMaterializationSchemaContract';
import { LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGER_SQL } from '../plugin-package/secret-binding/pluginPackageSecretBindingTargetSchemaContract';
import {
LOCAL_STEP_RUN_REFERENCE_TRIGGERS,
normalizeLocalSqliteSchemaSql,
} from '../run/stepRunSchemaContract';
export const LOCAL_SQLITE_CONTRACT_NAME = 'local-control-core';
export const LOCAL_SQLITE_CONTRACT_VERSION = 47;
export const LOCAL_SQLITE_CONTRACT_VERSION = 48;
const PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGERS = Object.freeze([
Object.freeze({
name: 'ql3_plugin_package_secret_binding_target_guard',
tableName: 'QingLong3PluginPackageSecretBindings',
sql: LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGER_SQL,
}),
]);
const PLUGIN_PACKAGE_SECRET_MATERIALIZATION_TRIGGERS = Object.freeze([
Object.freeze({
@@ -1752,6 +1761,7 @@ function assertRequiredSchema(client: DatabaseSync): number {
const expectedTriggers = [
...LOCAL_STEP_RUN_REFERENCE_TRIGGERS,
...PLUGIN_PACKAGE_AUTOMATION_DISPOSITION_TRIGGERS,
...PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGERS,
...PLUGIN_PACKAGE_SECRET_MATERIALIZATION_TRIGGERS,
].sort((left, right) => left.name.localeCompare(right.name));
if (
@@ -2564,10 +2574,10 @@ export async function auditLocalSqliteReadiness(
capability.contract_name !== LOCAL_SQLITE_CONTRACT_NAME ||
capability.contract_version !== LOCAL_SQLITE_CONTRACT_VERSION ||
capability.migration_id !==
'0093-plugin-package-secret-materialization-guard' ||
'0095-plugin-package-secret-binding-target-guard' ||
typeof capability.capabilities !== 'string' ||
capability.capabilities !==
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_project_administration":1,"local_security_audit":1,"local_security_audit_compaction":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_identity_credential_administration":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_secret_binding":1,"plugin_package_secret_materialization":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"plugin_package_quarantine":1,"plugin_package_lifecycle":1,"plugin_package_automation_publication":1,"plugin_package_automation_security_withdrawal":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_run_list":1,"run_attempt_log_retention":1,"plugin_package_workflow_task_attempt_admission":1}' ||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_project_administration":1,"local_security_audit":1,"local_security_audit_compaction":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_identity_credential_administration":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_secret_binding":1,"plugin_package_secret_binding_transition":1,"plugin_package_secret_materialization":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"plugin_package_quarantine":1,"plugin_package_lifecycle":1,"plugin_package_automation_publication":1,"plugin_package_automation_security_withdrawal":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_run_list":1,"run_attempt_log_retention":1,"plugin_package_workflow_task_attempt_admission":1}' ||
typeof capability.updated_at_ms !== 'number' ||
!Number.isSafeInteger(capability.updated_at_ms) ||
capability.updated_at_ms < 0
@@ -144,9 +144,11 @@ test('creates a reviewed edge database and opens runtime only after readiness',
'0092-capability-v46',
'0093-plugin-package-secret-materialization-guard',
'0094-capability-v47',
'0095-plugin-package-secret-binding-target-guard',
'0096-capability-v48',
]);
assert.equal(migrated.readiness.contractName, 'local-control-core');
assert.equal(migrated.readiness.contractVersion, 47);
assert.equal(migrated.readiness.contractVersion, 48);
assert.equal(migrated.readiness.journalMode, 'delete');
assert.equal(fs.statSync(databasePath).mode & 0o777, 0o600);
@@ -592,8 +594,8 @@ test('backfills v14 execution revisions with a verified independent digest', asy
.get(),
},
{
contract_version: 47,
migration_id: '0093-plugin-package-secret-materialization-guard',
contract_version: 48,
migration_id: '0095-plugin-package-secret-binding-target-guard',
},
);
} finally {
@@ -80,11 +80,38 @@ function fixture(boundAtMs = 100) {
return { binding, generation };
}
async function harness(active = true) {
async function harness(state = 'active') {
const client = new DatabaseSync(':memory:');
client.exec('PRAGMA foreign_keys = ON');
await migrateLocalSqliteDatabase(client);
const { binding, generation } = fixture();
const isActive = state === 'active';
const isStaged = state === 'staged';
const source = fixture();
const previousLockDigest = 'f'.repeat(64);
const generation = isStaged
? createPluginPackageResourceGeneration({
installationId: 'install-2',
projectId: 'project-1',
packageName: 'example-monitor',
lockDigest: LOCK_DIGEST,
generation: 2,
previousActiveLockDigest: previousLockDigest,
contentDigest: 'b'.repeat(64),
contents: MANIFEST.spec.contents,
})
: source.generation;
const binding = isStaged
? createPluginPackageSecretBinding({
generation,
manifest: MANIFEST,
assignments: source.binding.entries.map(({ name, secretRef }) => ({
name,
secretRef,
})),
authority: source.binding.authority,
boundAtMs: source.binding.boundAtMs,
})
: source.binding;
client
.prepare(
`INSERT INTO "QingLong3Projects"
@@ -100,11 +127,11 @@ async function harness(active = true) {
manifestDigest: binding.target.manifestDigest,
});
const recordJson = JSON.stringify({
installationId: 'install-1',
installationId: isStaged ? 'install-2' : 'install-1',
projectId: 'project-1',
packageName: 'example-monitor',
lockDigest: LOCK_DIGEST,
state: active ? 'active' : 'failed',
state,
version: 1,
recordDigest,
});
@@ -116,16 +143,18 @@ async function harness(active = true) {
previous_active_lock_digest, active_lock_digest, state, version,
last_mutation_id, last_mutation_digest, lock_json, record_json,
record_digest, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, '1.0.0', 'install', ?, 1, NULL, ?, ?, 1,
) VALUES (?, ?, ?, '1.0.0', 'install', ?, ?, ?, ?, ?, 1,
'mutation-1', ?, ?, ?, ?, 1, 1)`,
)
.run(
'install-1',
isStaged ? 'install-2' : 'install-1',
'project-1',
'example-monitor',
LOCK_DIGEST,
active ? LOCK_DIGEST : null,
active ? 'active' : 'failed',
generation.generation,
isStaged ? previousLockDigest : null,
isActive ? LOCK_DIGEST : isStaged ? previousLockDigest : null,
state,
'e'.repeat(64),
lockJson,
recordJson,
@@ -135,9 +164,47 @@ async function harness(active = true) {
.prepare(
`INSERT INTO "QingLong3PluginPackageInstallHeads"
(project_id, package_name, installation_id)
VALUES ('project-1', 'example-monitor', 'install-1')`,
VALUES ('project-1', 'example-monitor', ?)`,
)
.run();
.run(isStaged ? 'install-2' : 'install-1');
if (isStaged) {
const previousRecordDigest = '9'.repeat(64);
const previousLockJson = JSON.stringify({
lockDigest: previousLockDigest,
projectId: 'project-1',
packageName: 'example-monitor',
manifestDigest: '8'.repeat(64),
});
const previousRecordJson = JSON.stringify({
installationId: 'install-1',
projectId: 'project-1',
packageName: 'example-monitor',
lockDigest: previousLockDigest,
state: 'active',
version: 1,
recordDigest: previousRecordDigest,
});
client
.prepare(
`INSERT INTO "QingLong3PluginPackageInstalls" (
installation_id, project_id, package_name, package_version,
operation, lock_digest, target_generation,
previous_active_lock_digest, active_lock_digest, state, version,
last_mutation_id, last_mutation_digest, lock_json, record_json,
record_digest, created_at_ms, updated_at_ms
) VALUES ('install-1', 'project-1', 'example-monitor', '0.9.0',
'install', ?, 1, NULL, ?, 'active', 1,
'mutation-previous', ?, ?, ?, ?, 0, 0)`,
)
.run(
previousLockDigest,
previousLockDigest,
'7'.repeat(64),
previousLockJson,
previousRecordJson,
previousRecordDigest,
);
}
return {
client,
binding,
@@ -161,7 +228,7 @@ test('publishes and exact-replays one active generation binding', async (t) => {
});
test('rejects inactive targets and conflicting content', async (t) => {
const inactive = await harness(false);
const inactive = await harness('failed');
t.after(() => inactive.client.close());
await assert.rejects(
inactive.repository.publish(inactive.binding),
@@ -177,6 +244,21 @@ 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');
for (const state of ['queued', 'activating']) {
const rejected = await harness(state);
t.after(() => rejected.client.close());
await assert.rejects(
rejected.repository.publish(rejected.binding),
PluginPackageSecretBindingConflictError,
);
}
});
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, 47);
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 48);
});
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, 47);
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 48);
});
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, 47);
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 48);
});
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, 47);
assert.equal(prepared.writeContractVersion, 47);
assert.equal(LOCAL_SQLITE_WRITE_CONTRACT_VERSION, 47);
assert.equal(prepared.contractVersion, 48);
assert.equal(prepared.writeContractVersion, 48);
assert.equal(LOCAL_SQLITE_WRITE_CONTRACT_VERSION, 48);
assert.match(prepared.sha256, /^[0-9a-f]{64}$/);
assert.equal(prepared.bytes > 0, true);
assert.equal(prepared.pageCount > 0, true);