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
@@ -131,6 +131,16 @@ function database(serverVersionNum = '160014') {
})),
};
}
if (text.includes('FROM pg_trigger triggers')) {
return {
rows: contract.triggers.map((definition) => ({
triggerName: definition.name,
tableName: definition.tableName,
functionName: definition.functionName,
enabled: 'O',
})),
};
}
if (text.includes('FROM pg_catalog.pg_roles')) {
return {
rows: [
@@ -160,6 +170,7 @@ function database(serverVersionNum = '160014') {
'commit_plugin_package_lifecycle',
'commit_plugin_package_task_reconciliation',
'commit_plugin_package_quarantine',
'enforce_plugin_package_secret_binding_target',
'enforce_plugin_package_secret_materialization',
'enforce_plugin_package_stage_provenance',
'lock_active_plugin_package_project',
@@ -167,6 +167,16 @@ function database(serverVersionNum = '160014') {
})),
};
}
if (text.includes('FROM pg_trigger triggers')) {
return {
rows: contract.triggers.map((definition) => ({
triggerName: definition.name,
tableName: definition.tableName,
functionName: definition.functionName,
enabled: 'O',
})),
};
}
if (text.includes('FROM pg_catalog.pg_roles')) {
return {
rows: [
@@ -193,6 +203,7 @@ function database(serverVersionNum = '160014') {
rows: contract.functions.map(({ name: functionName }) => ({
functionName,
executeAllowed: ![
'enforce_plugin_package_secret_binding_target',
'enforce_plugin_package_secret_materialization',
'enforce_plugin_package_stage_provenance',
'plugin_package_automation_start_allowed',
@@ -301,6 +301,16 @@ function databaseResource(events, options = {}) {
})),
};
}
if (text.includes('FROM pg_trigger triggers')) {
return {
rows: contract.triggers.map((definition) => ({
triggerName: definition.name,
tableName: definition.tableName,
functionName: definition.functionName,
enabled: 'O',
})),
};
}
if (text.includes('FROM pg_catalog.pg_roles')) {
return {
rows: [
@@ -215,6 +215,16 @@ function databaseResource(events, overrides = {}) {
})),
};
}
if (text.includes('FROM pg_trigger triggers')) {
return {
rows: contract.triggers.map((definition) => ({
triggerName: definition.name,
tableName: definition.tableName,
functionName: definition.functionName,
enabled: 'O',
})),
};
}
if (text.includes('FROM pg_catalog.pg_roles')) {
return {
rows: [
@@ -313,5 +313,10 @@ export const postgresqlMainMigrationManifest: MigrationStreamManifest =
checksum:
'c995b7846ae8a57d3abb4b5523961e81aeba890e7405a030bcb505dfc6be3d25',
}),
Object.freeze({
id: 'pg-0062-plugin-package-secret-binding-target-guard',
checksum:
'cd4f92d8702da6b92dd9ae5153b5180400b94442f56393692b6ec038f998596b',
}),
]),
});
@@ -64,6 +64,7 @@ import { pg0058PluginPackageAutomationDispositionEventsMigration } from './pg-00
import { pg0059PluginPackageSecretBindingsMigration } from './pg-0059-plugin-package-secret-bindings';
import { pg0060PluginPackageSecretMaterializationGuardMigration } from './pg-0060-plugin-package-secret-materialization-guard';
import { pg0061PluginPackageSecretBindingApprovalPlansMigration } from './pg-0061-plugin-package-secret-binding-approval-plans';
import { pg0062PluginPackageSecretBindingTargetGuardMigration } from './pg-0062-plugin-package-secret-binding-target-guard';
export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMigrationContext> =
Object.freeze({
@@ -133,5 +134,6 @@ export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMi
pg0059PluginPackageSecretBindingsMigration,
pg0060PluginPackageSecretMaterializationGuardMigration,
pg0061PluginPackageSecretBindingApprovalPlansMigration,
pg0062PluginPackageSecretBindingTargetGuardMigration,
]),
});
@@ -0,0 +1,74 @@
import { CAPABILITIES_V60 } from './pg-0061-plugin-package-secret-binding-approval-plans';
import { definePostgresSqlMigration } from './sqlMigration';
export const CAPABILITIES_V61 = CAPABILITIES_V60.replace(
'"plugin_package_secret_binding":1,',
'"plugin_package_secret_binding":1,"plugin_package_secret_binding_transition":1,',
);
export const pg0062PluginPackageSecretBindingTargetGuardMigration =
definePostgresSqlMigration({
id: 'pg-0062-plugin-package-secret-binding-target-guard',
statements: [
`
CREATE FUNCTION "ql3"."enforce_plugin_package_secret_binding_target"()
RETURNS trigger
LANGUAGE plpgsql
VOLATILE
SET search_path = pg_catalog, ql3
AS $ql3$
BEGIN
PERFORM 1
FROM "ql3"."plugin_package_install_heads" AS head
JOIN "ql3"."plugin_package_installs" 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 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 "ql3"."plugin_package_installs" AS history
WHERE history.project_id = install.project_id
AND history.package_name = install.package_name
) AND
EXISTS (
SELECT 1
FROM "ql3"."plugin_package_installs" 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
)
)
)
FOR SHARE OF head, install;
IF NOT FOUND THEN
RAISE EXCEPTION
'Plugin Package Secret binding target is not current active or reviewed staged generation'
USING ERRCODE = 'check_violation';
END IF;
RETURN NEW;
END
$ql3$
`.trim(),
`REVOKE ALL ON FUNCTION "ql3"."enforce_plugin_package_secret_binding_target"() FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager, ql3_package_executor, ql3_worker_ingress`,
`CREATE TRIGGER ql3_plugin_package_secret_binding_target_guard BEFORE INSERT ON "ql3"."plugin_package_secret_bindings" FOR EACH ROW EXECUTE FUNCTION "ql3"."enforce_plugin_package_secret_binding_target"()`,
`DO $ql3$ BEGIN UPDATE "ql3"."schema_capabilities" SET contract_version = 61, migration_id = 'pg-0062-plugin-package-secret-binding-target-guard', capabilities = '${CAPABILITIES_V61}'::jsonb, updated_at_ms = floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint WHERE contract_name = 'control-core' AND contract_version = 60 AND migration_id = 'pg-0061-plugin-package-secret-binding-approval-plans' AND capabilities = '${CAPABILITIES_V60}'::jsonb; IF NOT FOUND THEN RAISE EXCEPTION 'control-core capability is not at version 60' USING ERRCODE = 'check_violation'; END IF; END $ql3$`,
],
});
@@ -193,10 +193,31 @@ export class PostgresPluginPackageSecretBindingRepository
AND install.project_id = $2
AND install.package_name = $3
AND install.lock_digest = $5
AND install.active_lock_digest = $5
AND install.target_generation = $6
AND install.state = 'active'
AND install.lock_json ->> 'manifestDigest' = $7
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 "ql3"."plugin_package_installs" AS history
WHERE history.project_id = install.project_id
AND history.package_name = install.package_name
) AND
EXISTS (
SELECT 1
FROM "ql3"."plugin_package_installs" 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
RETURNING generation_digest`,
[
@@ -217,7 +238,7 @@ export class PostgresPluginPackageSecretBindingRepository
const stored = await 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) {
@@ -12,11 +12,17 @@ export interface PostgresSchemaContractFunction {
readonly configuration: readonly string[];
}
export interface PostgresSchemaContractTrigger {
readonly name: string;
readonly tableName: string;
readonly functionName: string;
}
export interface PostgresSchemaContract {
readonly schema: 'ql3';
readonly contractName: 'control-core';
readonly contractVersion: 60;
readonly migrationId: 'pg-0061-plugin-package-secret-binding-approval-plans';
readonly contractVersion: 61;
readonly migrationId: 'pg-0062-plugin-package-secret-binding-target-guard';
readonly minimumServerMajor: 16;
readonly maximumServerMajor: 18;
readonly capabilities: Readonly<{
@@ -57,6 +63,7 @@ export interface PostgresSchemaContract {
plugin_package_materialized_revision: 1;
plugin_package_secret_binding: 1;
plugin_package_secret_binding_approval_plan: 1;
plugin_package_secret_binding_transition: 1;
plugin_package_secret_materialization: 1;
plugin_package_proposal: 1;
plugin_package_publisher_provenance: 1;
@@ -94,6 +101,7 @@ export interface PostgresSchemaContract {
readonly checks: readonly string[];
readonly foreignKeys: readonly string[];
readonly functions: readonly PostgresSchemaContractFunction[];
readonly triggers: readonly PostgresSchemaContractTrigger[];
}
function table(
@@ -107,8 +115,8 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
Object.freeze({
schema: 'ql3',
contractName: 'control-core',
contractVersion: 60,
migrationId: 'pg-0061-plugin-package-secret-binding-approval-plans',
contractVersion: 61,
migrationId: 'pg-0062-plugin-package-secret-binding-target-guard',
minimumServerMajor: 16,
maximumServerMajor: 18,
capabilities: Object.freeze({
@@ -142,6 +150,7 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
plugin_package_materialized_revision: 1,
plugin_package_secret_binding: 1,
plugin_package_secret_binding_approval_plan: 1,
plugin_package_secret_binding_transition: 1,
plugin_package_secret_materialization: 1,
plugin_package_proposal: 1,
plugin_package_publisher_provenance: 1,
@@ -2384,6 +2393,14 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
volatility: 'volatile',
configuration: Object.freeze(['search_path=pg_catalog, ql3']),
}),
Object.freeze({
name: 'enforce_plugin_package_secret_binding_target',
identityArguments: '',
owner: 'ql3_migration',
securityDefiner: false,
volatility: 'volatile',
configuration: Object.freeze(['search_path=pg_catalog, ql3']),
}),
Object.freeze({
name: 'enforce_plugin_package_secret_materialization',
identityArguments: '',
@@ -2516,4 +2533,16 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
configuration: Object.freeze(['search_path=pg_catalog, ql3']),
}),
]),
triggers: Object.freeze([
Object.freeze({
name: 'ql3_plugin_package_secret_binding_target_guard',
tableName: 'plugin_package_secret_bindings',
functionName: 'enforce_plugin_package_secret_binding_target',
}),
Object.freeze({
name: 'ql3_plugin_package_secret_materialization_guard',
tableName: 'plugin_package_materialized_revisions',
functionName: 'enforce_plugin_package_secret_materialization',
}),
]),
});
@@ -89,6 +89,13 @@ interface FunctionRow extends Record<string, unknown> {
publicExecute: unknown;
}
interface TriggerRow extends Record<string, unknown> {
triggerName: unknown;
tableName: unknown;
functionName: unknown;
enabled: unknown;
}
interface SchemaPrivilegeRow extends Record<string, unknown> {
schemaUsage: unknown;
schemaCreate: unknown;
@@ -1523,6 +1530,7 @@ const REQUIRED_RUNTIME_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges =
commit_plugin_package_quarantine: false,
commit_plugin_package_task_reconciliation: false,
enforce_plugin_package_secret_materialization: false,
enforce_plugin_package_secret_binding_target: false,
enforce_plugin_package_stage_provenance: false,
lock_active_plugin_package_project: false,
lock_approval_policy_fence: false,
@@ -1544,6 +1552,7 @@ const REQUIRED_PACKAGE_MANAGER_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges =
commit_plugin_package_quarantine: false,
commit_plugin_package_task_reconciliation: false,
enforce_plugin_package_secret_materialization: false,
enforce_plugin_package_secret_binding_target: false,
enforce_plugin_package_stage_provenance: false,
lock_active_plugin_package_project: false,
lock_approval_policy_fence: true,
@@ -1565,6 +1574,7 @@ const REQUIRED_PACKAGE_EXECUTOR_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges
commit_plugin_package_quarantine: true,
commit_plugin_package_task_reconciliation: true,
enforce_plugin_package_secret_materialization: false,
enforce_plugin_package_secret_binding_target: false,
enforce_plugin_package_stage_provenance: false,
lock_active_plugin_package_project: true,
lock_approval_policy_fence: true,
@@ -1728,7 +1738,13 @@ async function assertSchemaContract(
queryable: PostgresMigrationQueryable,
contract: PostgresSchemaContract,
): Promise<void> {
const [columnsResult, indexesResult, constraintsResult, functionsResult] =
const [
columnsResult,
indexesResult,
constraintsResult,
functionsResult,
triggersResult,
] =
await Promise.all([
queryable.query<ColumnRow>(
`
@@ -1803,6 +1819,27 @@ ORDER BY routines.proname, pg_get_function_identity_arguments(routines.oid)
`.trim(),
[contract.schema],
),
queryable.query<TriggerRow>(
`
SELECT
triggers.tgname AS "triggerName",
tables.relname AS "tableName",
routines.proname AS "functionName",
triggers.tgenabled AS "enabled"
FROM pg_trigger triggers
JOIN pg_class tables ON tables.oid = triggers.tgrelid
JOIN pg_namespace schemas ON schemas.oid = tables.relnamespace
JOIN pg_proc routines ON routines.oid = triggers.tgfoid
WHERE schemas.nspname = $1
AND NOT triggers.tgisinternal
AND triggers.tgname = ANY($2::text[])
ORDER BY triggers.tgname
`.trim(),
[
contract.schema,
contract.triggers.map(({ name }) => name),
],
),
]);
const actualTables = new Map<string, Set<string>>();
for (const row of columnsResult.rows) {
@@ -1943,6 +1980,35 @@ ORDER BY routines.proname, pg_get_function_identity_arguments(routines.oid)
findings.push(`unknown-function:${identity}`);
}
}
const actualTriggers = new Map<string, TriggerRow>();
for (const row of triggersResult.rows) {
if (
typeof row.triggerName !== 'string' ||
typeof row.tableName !== 'string' ||
typeof row.functionName !== 'string' ||
typeof row.enabled !== 'string'
) {
throw new PostgresSchemaReadinessError('schema_contract_invalid');
}
actualTriggers.set(row.triggerName, row);
}
for (const expected of contract.triggers) {
const actual = actualTriggers.get(expected.name);
if (!actual) {
findings.push(`missing-trigger:${expected.name}`);
continue;
}
if (
actual.tableName !== expected.tableName ||
actual.functionName !== expected.functionName ||
actual.enabled !== 'O'
) {
findings.push(`trigger-contract:${expected.name}`);
}
}
if (actualTriggers.size !== contract.triggers.length) {
findings.push('trigger-contract-row-count');
}
if (findings.length > 0) {
throw new PostgresSchemaReadinessError(
'schema_contract_invalid',
@@ -129,6 +129,14 @@ test('publishes, exact-replays and finds one binding', async () => {
value.queries.find(({ text }) => text.startsWith('INSERT')).text,
/install\.state = 'active'/,
);
assert.match(
value.queries.find(({ text }) => text.startsWith('INSERT')).text,
/install\.state = 'staged'/,
);
assert.match(
value.queries.find(({ text }) => text.startsWith('INSERT')).text,
/MAX\(history\.target_generation\)/,
);
});
test('rejects inactive targets and conflicting content', async () => {
@@ -0,0 +1,461 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
PluginPackageSecretBindingConflictError,
createPluginPackageSecretBinding,
} = require('@qinglong/runtime-core/plugin-package-secret-binding');
const {
createPluginPackageResourceGeneration,
} = require('@qinglong/runtime-core/plugin-package-resource-generation');
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
const {
assertPostgresPackageExecutorSchemaReady,
createPostgresDatabaseOpener,
PostgresPluginPackageSecretBindingRepository,
} = require('../dist/entrypoints/packageExecutor');
const {
runPostgresMigrations,
} = require('../dist/migration/migration');
const migrationConnectionString =
process.env.QL3_TEST_POSTGRES_MIGRATION_URL;
const executorConnectionString =
process.env.QL3_TEST_POSTGRES_PACKAGE_EXECUTOR_URL;
function manifest(packageName) {
return {
apiVersion: 'qinglong.io/v1alpha1',
kind: 'Package',
metadata: {
name: packageName,
displayName: packageName,
version: '1.0.0',
description: 'PostgreSQL Secret binding transition gate',
license: 'Apache-2.0',
},
spec: {
compatibility: {
qinglong: '>=3.0.0-0 <4.0.0',
architectures: ['arm64'],
deploymentProfiles: ['cluster-control'],
},
runtimes: [],
resources: {
memory: { recommended: '16Mi' },
disk: { install: '4Mi', working: '8Mi' },
},
permissions: {
network: { allowedHosts: [] },
secrets: [{ name: 'TOKEN', required: true }],
tools: [],
},
contents: { tasks: [], workflows: [], prompts: [], tools: [] },
},
};
}
function binding({
projectId,
packageName,
installationId,
lockDigest,
generation,
previousActiveLockDigest,
digestSeed,
}) {
const packageManifest = manifest(packageName);
const resourceGeneration = createPluginPackageResourceGeneration({
installationId,
projectId,
packageName,
lockDigest,
generation,
previousActiveLockDigest,
contentDigest: digestSeed.repeat(64),
contents: packageManifest.spec.contents,
});
return createPluginPackageSecretBinding({
generation: resourceGeneration,
manifest: packageManifest,
assignments: [
{
name: 'TOKEN',
secretRef: createSecretRef({
projectId,
name: 'runtime-token',
version: generation,
}),
},
],
authority: {
kind: 'approved-action-execution',
evidenceDigest: digestSeed.repeat(64),
},
boundAtMs: 100 + generation,
});
}
async function insertProject(pool, projectId) {
await pool.query(
`INSERT INTO "ql3"."projects" (
id, name, slug, status, version, created_at_ms, updated_at_ms
) VALUES ($1, $1, $1, 'active', 1, 1, 1)`,
[projectId],
);
}
async function insertInstall(
pool,
{
projectId,
packageName,
installationId,
lockDigest,
targetGeneration,
previousActiveLockDigest,
activeLockDigest,
state,
manifestDigest,
createdAtMs,
},
) {
const recordDigest = lockDigest;
const mutationDigest = lockDigest;
const lockJson = {
lockDigest,
projectId,
packageName,
manifestDigest,
};
const recordJson = {
installationId,
projectId,
packageName,
lockDigest,
state,
version: 1,
recordDigest,
};
await pool.query(
`INSERT INTO "ql3"."plugin_package_installs" (
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 (
$1, $2, $3, '1.0.0', $4, $5, $6, $7, $8, $9, 1,
$10, $11, $12::jsonb, $13::jsonb, $14, $15, $15
)`,
[
installationId,
projectId,
packageName,
targetGeneration === 1 ? 'install' : 'upgrade',
lockDigest,
targetGeneration,
previousActiveLockDigest,
activeLockDigest,
state,
`mutation-${installationId}`,
mutationDigest,
JSON.stringify(lockJson),
JSON.stringify(recordJson),
recordDigest,
createdAtMs,
],
);
}
async function insertHead(pool, value) {
await pool.query(
`INSERT INTO "ql3"."plugin_package_install_heads" (
project_id, package_name, installation_id
) VALUES ($1, $2, $3)`,
[value.projectId, value.packageName, value.installationId],
);
}
async function insertBindingDirectly(pool, value) {
return pool.query(
`INSERT INTO "ql3"."plugin_package_secret_bindings" (
generation_digest, project_id, package_name, installation_id,
lock_digest, generation, manifest_digest, authority_kind,
evidence_digest, bound_at_ms, binding_digest, binding_json
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12::jsonb
)`,
[
value.target.generationDigest,
value.target.projectId,
value.target.packageName,
value.target.installationId,
value.target.lockDigest,
value.target.generation,
value.target.manifestDigest,
value.authority.kind,
value.authority.evidenceDigest,
value.boundAtMs,
value.bindingDigest,
JSON.stringify(value),
],
);
}
async function open(role, connectionString) {
return createPostgresDatabaseOpener({
role,
connection: {
connectionString,
tls: { mode: 'disable' },
},
pool: {
maxConnections: 1,
applicationName: `ql3-b2-secret-binding-${role}`,
},
onPoolError(error) {
throw error;
},
})();
}
if (!migrationConnectionString || !executorConnectionString) {
test('PostgreSQL Secret binding transition gate requires migration and executor URLs', {
skip: true,
});
} else {
test('PostgreSQL enforces active and reviewed staged Secret binding targets', async () => {
const migrationDatabase = await open(
'migration',
migrationConnectionString,
);
let executorDatabase;
try {
await runPostgresMigrations({ pool: migrationDatabase.pool });
executorDatabase = await open(
'package-executor',
executorConnectionString,
);
const readiness = await assertPostgresPackageExecutorSchemaReady(
executorDatabase.pool,
);
assert.equal(readiness.ready, true);
assert.equal(readiness.contractVersion, 61);
const repository = new PostgresPluginPackageSecretBindingRepository(
executorDatabase.pool,
);
const namespace = `b2-${process.pid}-${Date.now()}`;
const activeProjectId = `${namespace}-active`;
const activePackageName = 'active-binding';
const activeInstallationId = `${namespace}-active-install`;
const activeLockDigest = 'a'.repeat(64);
const activeBinding = binding({
projectId: activeProjectId,
packageName: activePackageName,
installationId: activeInstallationId,
lockDigest: activeLockDigest,
generation: 1,
previousActiveLockDigest: null,
digestSeed: 'b',
});
await insertProject(migrationDatabase.pool, activeProjectId);
await insertInstall(migrationDatabase.pool, {
projectId: activeProjectId,
packageName: activePackageName,
installationId: activeInstallationId,
lockDigest: activeLockDigest,
targetGeneration: 1,
previousActiveLockDigest: null,
activeLockDigest,
state: 'active',
manifestDigest: activeBinding.target.manifestDigest,
createdAtMs: 1,
});
await insertHead(migrationDatabase.pool, {
projectId: activeProjectId,
packageName: activePackageName,
installationId: activeInstallationId,
});
assert.equal((await repository.publish(activeBinding)).status, 'created');
const stagedProjectId = `${namespace}-staged`;
const stagedPackageName = 'staged-binding';
const previousInstallationId = `${namespace}-previous-install`;
const stagedInstallationId = `${namespace}-staged-install`;
const previousLockDigest = 'c'.repeat(64);
const stagedLockDigest = 'd'.repeat(64);
const stagedBinding = binding({
projectId: stagedProjectId,
packageName: stagedPackageName,
installationId: stagedInstallationId,
lockDigest: stagedLockDigest,
generation: 2,
previousActiveLockDigest: previousLockDigest,
digestSeed: 'e',
});
await insertProject(migrationDatabase.pool, stagedProjectId);
await insertInstall(migrationDatabase.pool, {
projectId: stagedProjectId,
packageName: stagedPackageName,
installationId: previousInstallationId,
lockDigest: previousLockDigest,
targetGeneration: 1,
previousActiveLockDigest: null,
activeLockDigest: previousLockDigest,
state: 'active',
manifestDigest: stagedBinding.target.manifestDigest,
createdAtMs: 1,
});
await insertInstall(migrationDatabase.pool, {
projectId: stagedProjectId,
packageName: stagedPackageName,
installationId: stagedInstallationId,
lockDigest: stagedLockDigest,
targetGeneration: 2,
previousActiveLockDigest: previousLockDigest,
activeLockDigest: previousLockDigest,
state: 'staged',
manifestDigest: stagedBinding.target.manifestDigest,
createdAtMs: 2,
});
await insertHead(migrationDatabase.pool, {
projectId: stagedProjectId,
packageName: stagedPackageName,
installationId: stagedInstallationId,
});
assert.equal((await repository.publish(stagedBinding)).status, 'created');
assert.equal((await repository.publish(stagedBinding)).status, 'existing');
const activatingProjectId = `${namespace}-activating`;
const activatingPackageName = 'activating-binding';
const activatingPreviousId = `${namespace}-activating-previous`;
const activatingInstallationId = `${namespace}-activating-install`;
const activatingPreviousLock = 'f'.repeat(64);
const activatingLock = '1'.repeat(64);
const activatingBinding = binding({
projectId: activatingProjectId,
packageName: activatingPackageName,
installationId: activatingInstallationId,
lockDigest: activatingLock,
generation: 2,
previousActiveLockDigest: activatingPreviousLock,
digestSeed: '2',
});
await insertProject(migrationDatabase.pool, activatingProjectId);
await insertInstall(migrationDatabase.pool, {
projectId: activatingProjectId,
packageName: activatingPackageName,
installationId: activatingPreviousId,
lockDigest: activatingPreviousLock,
targetGeneration: 1,
previousActiveLockDigest: null,
activeLockDigest: activatingPreviousLock,
state: 'active',
manifestDigest: activatingBinding.target.manifestDigest,
createdAtMs: 1,
});
await insertInstall(migrationDatabase.pool, {
projectId: activatingProjectId,
packageName: activatingPackageName,
installationId: activatingInstallationId,
lockDigest: activatingLock,
targetGeneration: 2,
previousActiveLockDigest: activatingPreviousLock,
activeLockDigest: activatingPreviousLock,
state: 'activating',
manifestDigest: activatingBinding.target.manifestDigest,
createdAtMs: 2,
});
await insertHead(migrationDatabase.pool, {
projectId: activatingProjectId,
packageName: activatingPackageName,
installationId: activatingInstallationId,
});
await assert.rejects(
repository.publish(activatingBinding),
PluginPackageSecretBindingConflictError,
);
await assert.rejects(
insertBindingDirectly(executorDatabase.pool, activatingBinding),
(error) =>
error?.code === '23514' &&
/not current active or reviewed staged generation/.test(
error.message,
),
);
const staleProjectId = `${namespace}-stale`;
const stalePackageName = 'stale-binding';
const stalePreviousId = `${namespace}-stale-previous`;
const staleInstallationId = `${namespace}-stale-install`;
const newerInstallationId = `${namespace}-newer-install`;
const stalePreviousLock = '3'.repeat(64);
const staleLock = '4'.repeat(64);
const staleBinding = binding({
projectId: staleProjectId,
packageName: stalePackageName,
installationId: staleInstallationId,
lockDigest: staleLock,
generation: 2,
previousActiveLockDigest: stalePreviousLock,
digestSeed: '5',
});
await insertProject(migrationDatabase.pool, staleProjectId);
await insertInstall(migrationDatabase.pool, {
projectId: staleProjectId,
packageName: stalePackageName,
installationId: stalePreviousId,
lockDigest: stalePreviousLock,
targetGeneration: 1,
previousActiveLockDigest: null,
activeLockDigest: stalePreviousLock,
state: 'active',
manifestDigest: staleBinding.target.manifestDigest,
createdAtMs: 1,
});
await insertInstall(migrationDatabase.pool, {
projectId: staleProjectId,
packageName: stalePackageName,
installationId: staleInstallationId,
lockDigest: staleLock,
targetGeneration: 2,
previousActiveLockDigest: stalePreviousLock,
activeLockDigest: stalePreviousLock,
state: 'staged',
manifestDigest: staleBinding.target.manifestDigest,
createdAtMs: 2,
});
await insertInstall(migrationDatabase.pool, {
projectId: staleProjectId,
packageName: stalePackageName,
installationId: newerInstallationId,
lockDigest: '6'.repeat(64),
targetGeneration: 3,
previousActiveLockDigest: stalePreviousLock,
activeLockDigest: stalePreviousLock,
state: 'failed',
manifestDigest: staleBinding.target.manifestDigest,
createdAtMs: 3,
});
await insertHead(migrationDatabase.pool, {
projectId: staleProjectId,
packageName: stalePackageName,
installationId: staleInstallationId,
});
await assert.rejects(
repository.publish(staleBinding),
PluginPackageSecretBindingConflictError,
);
await assert.rejects(
insertBindingDirectly(executorDatabase.pool, staleBinding),
(error) => error?.code === '23514',
);
} finally {
if (executorDatabase) await executorDatabase.close();
await migrationDatabase.close();
}
});
}
@@ -112,6 +112,7 @@ test('defines the immutable PostgreSQL capability and Run core stream', async ()
'pg-0059-plugin-package-secret-bindings',
'pg-0060-plugin-package-secret-materialization-guard',
'pg-0061-plugin-package-secret-binding-approval-plans',
'pg-0062-plugin-package-secret-binding-target-guard',
],
);
for (const migration of postgresqlMainMigrationStream.migrations) {
@@ -554,6 +555,11 @@ test('freezes every published PostgreSQL migration checksum', () => {
checksum:
'c995b7846ae8a57d3abb4b5523961e81aeba890e7405a030bcb505dfc6be3d25',
},
{
id: 'pg-0062-plugin-package-secret-binding-target-guard',
checksum:
'cd4f92d8702da6b92dd9ae5153b5180400b94442f56393692b6ec038f998596b',
},
];
assert.deepEqual(
postgresqlMainMigrationStream.migrations.map(({ id, checksum }) => ({
@@ -2083,3 +2089,41 @@ test('advances capability v60 with least-privilege Package Secret binding approv
/migration_id = 'pg-0060-plugin-package-secret-materialization-guard'/,
);
});
test('advances capability v61 with a staged-target Secret binding database guard', async () => {
const migration = migrationById(
'pg-0062-plugin-package-secret-binding-target-guard',
);
const statements = [];
await migration.up({
async query(statement) {
statements.push(statement);
return { rows: [] };
},
});
const sql = statements.join('\n');
assert.match(
sql,
/CREATE FUNCTION "ql3"\."enforce_plugin_package_secret_binding_target"\(\)/,
);
assert.match(
sql,
/CREATE TRIGGER ql3_plugin_package_secret_binding_target_guard BEFORE INSERT/,
);
assert.match(sql, /install\.state = 'active'/);
assert.match(sql, /install\.state = 'staged'/);
assert.match(sql, /MAX\(history\.target_generation\)/);
assert.match(sql, /previous\.state = 'active'/);
assert.doesNotMatch(sql, /SECURITY DEFINER/);
assert.match(
sql,
/REVOKE ALL ON FUNCTION [^;]+ FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager, ql3_package_executor, ql3_worker_ingress/,
);
assert.match(sql, /contract_version = 61/);
assert.match(sql, /"plugin_package_secret_binding_transition":1/);
assert.match(sql, /contract_version = 60/);
assert.match(
sql,
/migration_id = 'pg-0061-plugin-package-secret-binding-approval-plans'/,
);
});
@@ -716,6 +716,19 @@ function queryable(overrides = {}) {
})),
};
}
if (text.includes('FROM pg_trigger triggers')) {
return {
rows: contract.triggers
.filter(({ name }) => name !== overrides.missingTrigger)
.map((definition) => ({
triggerName: definition.name,
tableName: definition.tableName,
functionName: definition.functionName,
enabled:
definition.name === overrides.disabledTrigger ? 'D' : 'O',
})),
};
}
if (text.includes('has_schema_privilege')) {
return {
rows: [
@@ -763,7 +776,7 @@ test('accepts the exact PostgreSQL control schema and least-privilege runtime ro
serverMajor: 16,
currentUser: 'ql3_runtime',
contractName: 'control-core',
contractVersion: 60,
contractVersion: 61,
migrationIds: [
'pg-0001-schema-capability',
'pg-0002-run-core',
@@ -826,6 +839,7 @@ test('accepts the exact PostgreSQL control schema and least-privilege runtime ro
'pg-0059-plugin-package-secret-bindings',
'pg-0060-plugin-package-secret-materialization-guard',
'pg-0061-plugin-package-secret-binding-approval-plans',
'pg-0062-plugin-package-secret-binding-target-guard',
],
});
});
@@ -856,10 +870,10 @@ test('accepts the exact schema and isolated least-privilege admin role', async (
}),
);
assert.equal(report.currentUser, 'ql3_admin');
assert.equal(report.contractVersion, 60);
assert.equal(report.contractVersion, 61);
assert.equal(
report.migrationIds.at(-1),
'pg-0061-plugin-package-secret-binding-approval-plans',
'pg-0062-plugin-package-secret-binding-target-guard',
);
});
@@ -872,10 +886,10 @@ test('accepts the isolated least-privilege automation manager role', async () =>
}),
);
assert.equal(report.currentUser, 'ql3_automation_manager');
assert.equal(report.contractVersion, 60);
assert.equal(report.contractVersion, 61);
assert.equal(
report.migrationIds.at(-1),
'pg-0061-plugin-package-secret-binding-approval-plans',
'pg-0062-plugin-package-secret-binding-target-guard',
);
const widened = automationManagerPrivileges();
@@ -904,10 +918,10 @@ test('accepts the isolated least-privilege human Approval manager role', async (
}),
);
assert.equal(report.currentUser, 'ql3_approval_manager');
assert.equal(report.contractVersion, 60);
assert.equal(report.contractVersion, 61);
assert.equal(
report.migrationIds.at(-1),
'pg-0061-plugin-package-secret-binding-approval-plans',
'pg-0062-plugin-package-secret-binding-target-guard',
);
const widened = approvalManagerPrivileges();
@@ -938,10 +952,10 @@ test('accepts the isolated least-privilege Run manager role', async () => {
}),
);
assert.equal(report.currentUser, 'ql3_run_manager');
assert.equal(report.contractVersion, 60);
assert.equal(report.contractVersion, 61);
assert.equal(
report.migrationIds.at(-1),
'pg-0061-plugin-package-secret-binding-approval-plans',
'pg-0062-plugin-package-secret-binding-target-guard',
);
const widened = runManagerPrivileges();
@@ -1073,10 +1087,10 @@ test('accepts the exact schema and isolated Worker ingress role', async () => {
}),
);
assert.equal(report.currentUser, 'ql3_worker_ingress');
assert.equal(report.contractVersion, 60);
assert.equal(report.contractVersion, 61);
assert.equal(
report.migrationIds.at(-1),
'pg-0061-plugin-package-secret-binding-approval-plans',
'pg-0062-plugin-package-secret-binding-target-guard',
);
});
@@ -1159,6 +1173,35 @@ test('rejects unknown ql3 objects and an over-privileged runtime role', async ()
);
});
test('fails closed when a reviewed Package Secret trigger is missing or disabled', async () => {
await assert.rejects(
assertPostgresSchemaReady(
queryable({
missingTrigger: 'ql3_plugin_package_secret_binding_target_guard',
}),
),
(error) =>
error instanceof PostgresSchemaReadinessError &&
error.code === 'schema_contract_invalid' &&
error.facts.includes(
'missing-trigger:ql3_plugin_package_secret_binding_target_guard',
),
);
await assert.rejects(
assertPostgresSchemaReady(
queryable({
disabledTrigger: 'ql3_plugin_package_secret_materialization_guard',
}),
),
(error) =>
error instanceof PostgresSchemaReadinessError &&
error.code === 'schema_contract_invalid' &&
error.facts.includes(
'trigger-contract:ql3_plugin_package_secret_materialization_guard',
),
);
});
test('preserves database availability errors for the outer readiness layer', async () => {
const unavailable = new Error('database unavailable');
let calls = 0;
@@ -363,9 +363,9 @@ function composeDockerHarness(
'/opt/qinglong/node_modules/@qinglong/local-application/dist/cli.js',
],
Labels: {
'io.qinglong.local.sqlite-contract-min': '47',
'io.qinglong.local.sqlite-contract-max': '47',
'io.qinglong.local.sqlite-write-contract': '47',
'io.qinglong.local.sqlite-contract-min': '48',
'io.qinglong.local.sqlite-contract-max': '48',
'io.qinglong.local.sqlite-write-contract': '48',
'io.qinglong.local.application-config': '2',
'io.qinglong.local.compose-selection': '1',
'io.qinglong.ai': 'excluded',
@@ -975,9 +975,9 @@ test('preflights exact local image, Compose merge and SQLite capability', async
'/opt/qinglong/node_modules/@qinglong/local-application/dist/cli.js',
],
Labels: {
'io.qinglong.local.sqlite-contract-min': '47',
'io.qinglong.local.sqlite-contract-max': '47',
'io.qinglong.local.sqlite-write-contract': '47',
'io.qinglong.local.sqlite-contract-min': '48',
'io.qinglong.local.sqlite-contract-max': '48',
'io.qinglong.local.sqlite-write-contract': '48',
'io.qinglong.local.application-config': '2',
'io.qinglong.local.compose-selection': '1',
'io.qinglong.ai': 'excluded',
@@ -1029,7 +1029,7 @@ test('preflights exact local image, Compose merge and SQLite capability', async
assert.equal(result.status, 'ready');
assert.equal(result.generation, 1);
assert.equal(result.profile, 'edge');
assert.equal(result.sqlite.contractVersion, 47);
assert.equal(result.sqlite.contractVersion, 48);
assert.equal(result.image.architecture, 'arm64');
assert.equal(calls.length, 2);
assert.deepEqual(calls[0].slice(0, 2), ['image', 'inspect']);
@@ -1129,8 +1129,8 @@ test('applies one Compose generation and exactly replays its health receipt', as
assert.equal(mode(receiptPath), 0o600);
const receipt = JSON.parse(fs.readFileSync(receiptPath, 'utf8'));
assert.deepEqual(receipt.sqlite, {
contractVersion: 47,
writeContractVersion: 47,
contractVersion: 48,
writeContractVersion: 48,
writeObservation: 'unchanged',
backup: null,
});
@@ -1427,8 +1427,8 @@ test('rolls a failed Compose candidate forward to a healthy prior digest', async
`${command.request.rolloutId}.sqlite`,
);
assert.equal(mode(backupPath), 0o600);
assert.equal(receipt.sqlite.contractVersion, 47);
assert.equal(receipt.sqlite.writeContractVersion, 47);
assert.equal(receipt.sqlite.contractVersion, 48);
assert.equal(receipt.sqlite.writeContractVersion, 48);
assert.equal(receipt.sqlite.writeObservation, 'changed');
assert.match(receipt.sqlite.backup.sha256, /^[0-9a-f]{64}$/);
assert.equal(receipt.sqlite.backup.bytes > 0, true);
@@ -34,8 +34,8 @@ test('inspects the exact fresh Profile schema without exposing its path', async
assert.equal(result.status, 'ready');
assert.equal(result.profile, 'edge');
assert.equal(result.storage.contractName, 'local-control-core');
assert.equal(result.storage.contractVersion, 47);
assert.equal(result.storage.migrationCount, 94);
assert.equal(result.storage.contractVersion, 48);
assert.equal(result.storage.migrationCount, 96);
assert.equal(result.storage.journalMode, 'delete');
assert.equal(JSON.stringify(result).includes(state.directory), false);
});
@@ -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);