feat(ql3): atomically withdraw quarantined automation

This commit is contained in:
whyour
2026-08-13 03:42:38 +08:00
parent a6ad636251
commit 4c2a0b6adf
39 changed files with 983 additions and 264 deletions
@@ -98,6 +98,8 @@ import { local0085PluginPackageWorkflowRunListIndexMigration } from '../migratio
import { local0086CapabilityV43Migration } from '../migrations/0086-capability-v43';
import { local0087RunAttemptLogRetentionMigration } from '../migrations/0087-run-attempt-log-retention';
import { local0088CapabilityV44Migration } from '../migrations/0088-capability-v44';
import { local0089PluginPackageAutomationDispositionEventsMigration } from '../migrations/0089-plugin-package-automation-disposition-events';
import { local0090CapabilityV45Migration } from '../migrations/0090-capability-v45';
import type { LocalSqliteMigrationContext } from '../migrations/sqlMigration';
import {
LOCAL_SQLITE_MIGRATION_STREAM_ID,
@@ -208,6 +210,8 @@ export const localSqliteMigrationDefinition: MigrationStreamDefinition<LocalSqli
local0086CapabilityV43Migration,
local0087RunAttemptLogRetentionMigration,
local0088CapabilityV44Migration,
local0089PluginPackageAutomationDispositionEventsMigration,
local0090CapabilityV45Migration,
]),
});
@@ -452,5 +452,15 @@ export const localSqliteMigrationManifest: MigrationStreamManifest =
checksum:
'c47a61b140b54d448c30ce7d5f7927c0d16fb897ab36dbe1d6010da2c39075a7',
}),
Object.freeze({
id: '0089-plugin-package-automation-disposition-events',
checksum:
'3eaff9c7621fc4a69a7605fd7de29d38dfde856df13b5e467ccc2bf1245e21aa',
}),
Object.freeze({
id: '0090-capability-v45',
checksum:
'1919987d29ef581e150116c590f1dc98f5d327791fc6425d5f1a27b8f6de5475',
}),
]),
});
@@ -70,9 +70,7 @@ CREATE TABLE IF NOT EXISTS "QingLong3SchemaMigrations" (
).map(record);
}
async findById(
migrationId: string,
): Promise<MigrationStreamRecord | null> {
async findById(migrationId: string): Promise<MigrationStreamRecord | null> {
const row = this.client
.prepare(
`SELECT migration_id, stream_id, dialect, checksum, applied_at_ms
@@ -88,6 +86,11 @@ CREATE TABLE IF NOT EXISTS "QingLong3SchemaMigrations" (
transaction: MigrationStreamTransaction<LocalSqliteMigrationContext>,
) => Promise<T>,
): Promise<T> {
const foreignKeys = this.client.prepare('PRAGMA foreign_keys').get() as
| { foreign_keys?: unknown }
| undefined;
const restoreForeignKeys = foreignKeys?.foreign_keys === 1;
if (restoreForeignKeys) this.client.exec('PRAGMA foreign_keys = OFF');
this.client.exec('BEGIN IMMEDIATE');
try {
const result = await work({
@@ -116,9 +119,11 @@ CREATE TABLE IF NOT EXISTS "QingLong3SchemaMigrations" (
},
});
this.client.exec('COMMIT');
if (restoreForeignKeys) this.client.exec('PRAGMA foreign_keys = ON');
return result;
} catch (error) {
if (this.client.isTransaction) this.client.exec('ROLLBACK');
if (restoreForeignKeys) this.client.exec('PRAGMA foreign_keys = ON');
throw error;
}
}
@@ -0,0 +1,105 @@
import { defineLocalSqliteMigration } from './sqlMigration';
export const local0089PluginPackageAutomationDispositionEventsMigration =
defineLocalSqliteMigration({
id: '0089-plugin-package-automation-disposition-events',
statements: [
`
CREATE TABLE "QingLong3PluginPackageAutomationDispositionEvents" (
event_digest TEXT PRIMARY KEY NOT NULL,
event_kind TEXT NOT NULL
CONSTRAINT ql3_plugin_package_automation_disposition_kind_check
CHECK (event_kind IN ('lifecycle','quarantine')),
CONSTRAINT ql3_plugin_package_automation_disposition_digest_check CHECK (
length(event_digest) = 64 AND
event_digest NOT GLOB '*[^0-9a-f]*'
)
)
`,
`INSERT INTO "QingLong3PluginPackageAutomationDispositionEvents" (event_digest, event_kind) SELECT event_digest, 'lifecycle' FROM "QingLong3PluginPackageLifecycleEvents"`,
`INSERT OR IGNORE INTO "QingLong3PluginPackageAutomationDispositionEvents" (event_digest, event_kind) SELECT event_digest, 'quarantine' FROM "QingLong3PluginPackageQuarantineEvents"`,
`CREATE TRIGGER ql3_plugin_package_automation_lifecycle_disposition_insert AFTER INSERT ON "QingLong3PluginPackageLifecycleEvents" BEGIN INSERT OR IGNORE INTO "QingLong3PluginPackageAutomationDispositionEvents" (event_digest, event_kind) VALUES (NEW.event_digest, 'lifecycle'); END`,
`CREATE TRIGGER ql3_plugin_package_automation_quarantine_disposition_insert AFTER INSERT ON "QingLong3PluginPackageQuarantineEvents" BEGIN INSERT OR IGNORE INTO "QingLong3PluginPackageAutomationDispositionEvents" (event_digest, event_kind) VALUES (NEW.event_digest, 'quarantine'); END`,
`PRAGMA defer_foreign_keys = ON`,
`PRAGMA legacy_alter_table = ON`,
`ALTER TABLE "QingLong3PluginPackageAutomationPublications" RENAME TO "QingLong3PluginPackageAutomationPublicationsBeforeDisposition"`,
`
CREATE TABLE "QingLong3PluginPackageAutomationPublications" (
publication_digest TEXT PRIMARY KEY NOT NULL,
project_id TEXT NOT NULL,
package_name TEXT NOT NULL,
installation_id TEXT NOT NULL,
lock_digest TEXT NOT NULL,
generation INTEGER NOT NULL,
generation_digest TEXT NOT NULL,
materialized_revision_digest TEXT NOT NULL,
state TEXT NOT NULL,
version INTEGER NOT NULL,
previous_publication_digest TEXT,
lifecycle_event_digest TEXT,
published_at_ms INTEGER NOT NULL,
publication_json TEXT NOT NULL,
CONSTRAINT ql3_plugin_package_automation_publication_revision_fk
FOREIGN KEY (generation_digest)
REFERENCES "QingLong3PluginPackageMaterializedRevisions" (generation_digest)
ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT ql3_plugin_package_automation_publication_previous_fk
FOREIGN KEY (previous_publication_digest)
REFERENCES "QingLong3PluginPackageAutomationPublications" (publication_digest)
ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT ql3_plugin_package_automation_publication_disposition_fk
FOREIGN KEY (lifecycle_event_digest)
REFERENCES "QingLong3PluginPackageAutomationDispositionEvents" (event_digest)
ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT ql3_plugin_package_automation_publication_identity_check CHECK (
length(project_id) BETWEEN 1 AND 128 AND
length(package_name) BETWEEN 1 AND 63 AND
length(installation_id) BETWEEN 1 AND 128 AND
generation BETWEEN 1 AND 2147483647 AND
state IN ('active','withdrawn','absent') AND
version BETWEEN 1 AND 2147483647 AND published_at_ms >= 0 AND
(version = 1 AND state IN ('active','absent') AND
previous_publication_digest IS NULL AND lifecycle_event_digest IS NULL OR
version > 1 AND previous_publication_digest IS NOT NULL) AND
(state <> 'withdrawn' OR lifecycle_event_digest IS NOT NULL)
),
CONSTRAINT ql3_plugin_package_automation_publication_digest_check CHECK (
length(publication_digest) = 64 AND publication_digest NOT GLOB '*[^0-9a-f]*' AND
length(lock_digest) = 64 AND lock_digest NOT GLOB '*[^0-9a-f]*' AND
length(generation_digest) = 64 AND generation_digest NOT GLOB '*[^0-9a-f]*' AND
length(materialized_revision_digest) = 64 AND materialized_revision_digest NOT GLOB '*[^0-9a-f]*' AND
(previous_publication_digest IS NULL OR length(previous_publication_digest) = 64 AND previous_publication_digest NOT GLOB '*[^0-9a-f]*') AND
(lifecycle_event_digest IS NULL OR length(lifecycle_event_digest) = 64 AND lifecycle_event_digest NOT GLOB '*[^0-9a-f]*')
),
CONSTRAINT ql3_plugin_package_automation_publication_json_check CHECK (
length(CAST(publication_json AS BLOB)) BETWEEN 2 AND 12582912 AND
json_valid(publication_json) AND json_type(publication_json) = 'object' AND
json_extract(publication_json, '$.schema') = 'qinglong/plugin-package-automation-publication@v1' AND
json_extract(publication_json, '$.target.projectId') = project_id AND
json_extract(publication_json, '$.target.packageName') = package_name AND
json_extract(publication_json, '$.target.installationId') = installation_id AND
json_extract(publication_json, '$.target.lockDigest') = lock_digest AND
json_extract(publication_json, '$.target.generation') = generation AND
json_extract(publication_json, '$.target.generationDigest') = generation_digest AND
json_extract(publication_json, '$.target.materializedRevisionDigest') = materialized_revision_digest AND
json_extract(publication_json, '$.state') = state AND
json_extract(publication_json, '$.version') = version AND
(previous_publication_digest IS NULL AND json_type(publication_json, '$.previousPublicationDigest') = 'null' OR json_extract(publication_json, '$.previousPublicationDigest') = previous_publication_digest) AND
(lifecycle_event_digest IS NULL AND json_type(publication_json, '$.lifecycleEventDigest') = 'null' OR json_extract(publication_json, '$.lifecycleEventDigest') = lifecycle_event_digest) AND
json_extract(publication_json, '$.publishedAtMs') = published_at_ms AND
json_extract(publication_json, '$.publicationDigest') = publication_digest AND
json_type(publication_json, '$.definitions.workflows') = 'array' AND
json_type(publication_json, '$.definitions.prompts') = 'array' AND
(state = 'absent' AND json_array_length(json_extract(publication_json, '$.definitions.workflows')) + json_array_length(json_extract(publication_json, '$.definitions.prompts')) = 0 OR
state <> 'absent' AND json_array_length(json_extract(publication_json, '$.definitions.workflows')) + json_array_length(json_extract(publication_json, '$.definitions.prompts')) > 0)
)
)
`,
`INSERT INTO "QingLong3PluginPackageAutomationPublications" SELECT * FROM "QingLong3PluginPackageAutomationPublicationsBeforeDisposition" ORDER BY project_id, package_name, version`,
`DROP TABLE "QingLong3PluginPackageAutomationPublicationsBeforeDisposition"`,
`CREATE UNIQUE INDEX ql3_plugin_package_automation_publication_version_uidx ON "QingLong3PluginPackageAutomationPublications" (project_id, package_name, version)`,
`CREATE UNIQUE INDEX ql3_plugin_package_automation_publication_previous_uidx ON "QingLong3PluginPackageAutomationPublications" (previous_publication_digest) WHERE previous_publication_digest IS NOT NULL`,
`CREATE INDEX ql3_plugin_package_automation_publication_generation_idx ON "QingLong3PluginPackageAutomationPublications" (generation_digest, publication_digest)`,
`PRAGMA legacy_alter_table = OFF`,
],
});
@@ -0,0 +1,14 @@
import { CAPABILITIES_V44 } from './0088-capability-v44';
import { defineLocalSqliteMigration } from './sqlMigration';
export const CAPABILITIES_V45 = CAPABILITIES_V44.replace(
'"plugin_package_automation_publication":1,',
'"plugin_package_automation_publication":1,"plugin_package_automation_security_withdrawal":1,',
);
export const local0090CapabilityV45Migration = defineLocalSqliteMigration({
id: '0090-capability-v45',
statements: [
`UPDATE "QingLong3SchemaCapabilities" SET contract_version = 45, migration_id = '0089-plugin-package-automation-disposition-events', capabilities = '${CAPABILITIES_V45}', updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER) WHERE contract_name = 'local-control-core' AND contract_version = 44 AND migration_id = '0087-run-attempt-log-retention' AND capabilities = '${CAPABILITIES_V44}'`,
],
});
@@ -44,7 +44,10 @@ function integer(row: Row, key: string): number {
return value as number;
}
function targetIdentity(projectId: unknown, packageName: unknown): {
function targetIdentity(
projectId: unknown,
packageName: unknown,
): {
readonly projectId: string;
readonly packageName: string;
} {
@@ -119,19 +122,18 @@ export class LocalSqlitePluginPackageAutomationPublicationRepository
#parse(row: Row): Readonly<PluginPackageAutomationPublication> {
try {
const publication = normalizePluginPackageAutomationPublication(
JSON.parse(text(row, 'publicationJson')) as
PluginPackageAutomationPublication,
JSON.parse(
text(row, 'publicationJson'),
) as PluginPackageAutomationPublication,
);
if (
publication.publicationDigest !==
text(row, 'publicationDigest') ||
publication.publicationDigest !== text(row, 'publicationDigest') ||
publication.target.projectId !== text(row, 'projectId') ||
publication.target.packageName !== text(row, 'packageName') ||
publication.target.installationId !== text(row, 'installationId') ||
publication.target.lockDigest !== text(row, 'lockDigest') ||
publication.target.generation !== integer(row, 'generation') ||
publication.target.generationDigest !==
text(row, 'generationDigest') ||
publication.target.generationDigest !== text(row, 'generationDigest') ||
publication.target.materializedRevisionDigest !==
text(row, 'materializedRevisionDigest') ||
publication.state !== text(row, 'state') ||
@@ -142,9 +144,7 @@ export class LocalSqlitePluginPackageAutomationPublicationRepository
}
return publication;
} catch (error) {
if (
error instanceof PluginPackageAutomationPublicationUnavailableError
) {
if (error instanceof PluginPackageAutomationPublicationUnavailableError) {
throw error;
}
throw new PluginPackageAutomationPublicationUnavailableError();
@@ -389,8 +389,9 @@ export class LocalSqlitePluginPackageAutomationPublicationRepository
return this.#findCurrent(projectId, packageName);
}
publishInTransaction(
#publishInTransaction(
value: Readonly<PluginPackageAutomationPublication>,
securityWithdrawal: boolean,
): Readonly<{
status: 'created' | 'existing';
publication: Readonly<PluginPackageAutomationPublication>;
@@ -430,10 +431,7 @@ export class LocalSqlitePluginPackageAutomationPublicationRepository
);
}
try {
assertPluginPackageAutomationPublicationSuccessor(
current,
publication,
);
assertPluginPackageAutomationPublicationSuccessor(current, publication);
} catch (error) {
if (error instanceof InvalidPluginPackageAutomationPublicationError) {
throw new PluginPackageAutomationPublicationConflictError(
@@ -467,33 +465,35 @@ export class LocalSqlitePluginPackageAutomationPublicationRepository
'materialized revision fence does not match publication target',
);
}
const securityFence = client
.prepare(
`SELECT EXISTS (
SELECT 1
FROM "QingLong3PluginPackageQuarantineEvents" AS quarantine
WHERE quarantine.project_id = ?
AND quarantine.package_name = ?
AND quarantine.installation_id = ?
AND quarantine.lock_digest = ?
) AS "blocked"`,
)
.get(
publication.target.projectId,
publication.target.packageName,
publication.target.installationId,
publication.target.lockDigest,
) as Row | undefined;
if (
!securityFence ||
(securityFence.blocked !== 0 && securityFence.blocked !== 1)
) {
throw new PluginPackageAutomationPublicationUnavailableError();
}
if (securityFence.blocked === 1) {
throw new PluginPackageAutomationPublicationConflictError(
'quarantined Package generation cannot publish automation',
);
if (!securityWithdrawal) {
const securityFence = client
.prepare(
`SELECT EXISTS (
SELECT 1
FROM "QingLong3PluginPackageQuarantineEvents" AS quarantine
WHERE quarantine.project_id = ?
AND quarantine.package_name = ?
AND quarantine.installation_id = ?
AND quarantine.lock_digest = ?
) AS "blocked"`,
)
.get(
publication.target.projectId,
publication.target.packageName,
publication.target.installationId,
publication.target.lockDigest,
) as Row | undefined;
if (
!securityFence ||
(securityFence.blocked !== 0 && securityFence.blocked !== 1)
) {
throw new PluginPackageAutomationPublicationUnavailableError();
}
if (securityFence.blocked === 1) {
throw new PluginPackageAutomationPublicationConflictError(
'quarantined Package generation cannot publish automation',
);
}
}
client
.prepare(
@@ -570,9 +570,31 @@ export class LocalSqlitePluginPackageAutomationPublicationRepository
});
}
publish(
publishInTransaction(
value: Readonly<PluginPackageAutomationPublication>,
): Promise<
): Readonly<{
status: 'created' | 'existing';
publication: Readonly<PluginPackageAutomationPublication>;
}> {
return this.#publishInTransaction(value, false);
}
publishSecurityWithdrawalInTransaction(
value: Readonly<PluginPackageAutomationPublication>,
): Readonly<{
status: 'created' | 'existing';
publication: Readonly<PluginPackageAutomationPublication>;
}> {
const publication = normalizePluginPackageAutomationPublication(value);
if (publication.state !== 'withdrawn') {
throw new PluginPackageAutomationPublicationConflictError(
'security withdrawal must narrow automation state',
);
}
return this.#publishInTransaction(publication, true);
}
publish(value: Readonly<PluginPackageAutomationPublication>): Promise<
Readonly<{
status: 'created' | 'existing';
publication: Readonly<PluginPackageAutomationPublication>;
@@ -20,6 +20,7 @@ import {
normalizePluginPackageInstallRecord,
type PluginPackageInstallRecord,
} from '@qinglong/runtime-core/plugin-package-install';
import { createPluginPackageAutomationLifecyclePublication } from '@qinglong/runtime-core/plugin-package-automation-publication';
import {
createProjectToolDefinitionSnapshot,
normalizeProjectToolDefinitionSnapshot,
@@ -40,6 +41,7 @@ import {
} from '@qinglong/runtime-core/task-spec-semantic';
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
import { LocalSqlitePluginPackageAutomationPublicationRepository } from './pluginPackageAutomationPublicationRepository';
type Row = Record<string, unknown>;
@@ -314,6 +316,30 @@ export class LocalSqlitePluginPackageQuarantineRepository
if (error instanceof PluginPackageQuarantineUnavailableError) throw error;
throw new PluginPackageQuarantineUnavailableError();
}
const automation = this.#authority.client
.prepare(
`SELECT state,
lifecycle_event_digest AS "lifecycleEventDigest"
FROM "QingLong3PluginPackageAutomationPublications"
WHERE project_id = ? AND package_name = ?
AND installation_id = ? AND lock_digest = ?
ORDER BY version DESC
LIMIT 1`,
)
.get(
receipt.target.projectId,
receipt.target.packageName,
receipt.target.installationId,
receipt.target.lockDigest,
) as Row | undefined;
if (
automation &&
(text(automation, 'state') === 'active' ||
(automation.lifecycleEventDigest === receipt.eventDigest &&
text(automation, 'state') !== 'withdrawn'))
) {
throw new PluginPackageQuarantineUnavailableError();
}
}
#findStored(
@@ -433,6 +459,40 @@ export class LocalSqlitePluginPackageQuarantineRepository
return record;
}
#withdrawAutomation(
event: Readonly<PluginPackageQuarantineEvent>,
record: Readonly<PluginPackageInstallRecord>,
committedAtMs: number,
): void {
const publications =
new LocalSqlitePluginPackageAutomationPublicationRepository(
this.#authority,
);
const current = publications.findCurrentInTransaction(
event.target.projectId,
event.target.packageName,
);
if (!current) return;
if (
current.target.installationId !== event.target.installationId ||
current.target.lockDigest !== event.target.lockDigest ||
current.target.generation !== record.targetGeneration
) {
throw new PluginPackageQuarantineConflictError(
'Workflow/Prompt publication does not match the quarantined Package generation',
);
}
if (current.state === 'absent' || current.state === 'withdrawn') return;
publications.publishSecurityWithdrawalInTransaction(
createPluginPackageAutomationLifecyclePublication({
previous: current,
state: 'withdrawn',
lifecycleEventDigest: event.eventDigest,
publishedAtMs: committedAtMs,
}),
);
}
#activeContributions(
projectId: string,
): readonly Readonly<ProjectToolDefinitionSnapshotContribution>[] {
@@ -792,7 +852,7 @@ export class LocalSqlitePluginPackageQuarantineRepository
'target lock is already quarantined by another event',
);
}
this.#install(event);
const install = this.#install(event);
const clock = client
.prepare(
`SELECT CAST(unixepoch('subsec') * 1000 AS INTEGER) AS "nowMs"`,
@@ -857,6 +917,7 @@ export class LocalSqlitePluginPackageQuarantineRepository
),
);
this.#insertEvent(event);
this.#withdrawAutomation(event, install, committedAtMs);
this.#publishSnapshot(snapshot, committedAtMs);
const receipt = createPluginPackageWithdrawalReceipt({
eventDigest: event.eventDigest,
@@ -8,7 +8,20 @@ import {
} from '../run/stepRunSchemaContract';
export const LOCAL_SQLITE_CONTRACT_NAME = 'local-control-core';
export const LOCAL_SQLITE_CONTRACT_VERSION = 44;
export const LOCAL_SQLITE_CONTRACT_VERSION = 45;
const PLUGIN_PACKAGE_AUTOMATION_DISPOSITION_TRIGGERS = Object.freeze([
Object.freeze({
name: 'ql3_plugin_package_automation_lifecycle_disposition_insert',
tableName: 'QingLong3PluginPackageLifecycleEvents',
sql: `CREATE TRIGGER ql3_plugin_package_automation_lifecycle_disposition_insert AFTER INSERT ON "QingLong3PluginPackageLifecycleEvents" BEGIN INSERT OR IGNORE INTO "QingLong3PluginPackageAutomationDispositionEvents" (event_digest, event_kind) VALUES (NEW.event_digest, 'lifecycle'); END`,
}),
Object.freeze({
name: 'ql3_plugin_package_automation_quarantine_disposition_insert',
tableName: 'QingLong3PluginPackageQuarantineEvents',
sql: `CREATE TRIGGER ql3_plugin_package_automation_quarantine_disposition_insert AFTER INSERT ON "QingLong3PluginPackageQuarantineEvents" BEGIN INSERT OR IGNORE INTO "QingLong3PluginPackageAutomationDispositionEvents" (event_digest, event_kind) VALUES (NEW.event_digest, 'quarantine'); END`,
}),
]);
const OPTIONAL_FEATURE_TABLE_NAMES = new Set([
'QingLong3AiSchemaMigrations',
@@ -1007,6 +1020,10 @@ const REQUIRED_SCHEMA = Object.freeze({
'ql3_plugin_package_automation_publication_generation_idx',
]),
}),
QingLong3PluginPackageAutomationDispositionEvents: Object.freeze({
columns: Object.freeze(['event_digest', 'event_kind']),
indexes: Object.freeze([]),
}),
QingLong3PluginPackageAutomationPublicationHeads: Object.freeze({
columns: Object.freeze([
'project_id',
@@ -1702,9 +1719,10 @@ function assertRequiredSchema(client: DatabaseSync): number {
ORDER BY name`,
)
.all(...ownedTableNames) as unknown as TriggerRow[];
const expectedTriggers = [...LOCAL_STEP_RUN_REFERENCE_TRIGGERS].sort(
(left, right) => left.name.localeCompare(right.name),
);
const expectedTriggers = [
...LOCAL_STEP_RUN_REFERENCE_TRIGGERS,
...PLUGIN_PACKAGE_AUTOMATION_DISPOSITION_TRIGGERS,
].sort((left, right) => left.name.localeCompare(right.name));
if (
triggerRows.length !== expectedTriggers.length ||
triggerRows.some((row, index) => {
@@ -2097,8 +2115,15 @@ function assertPluginPackageAutomationPublicationIntegrity(
LEFT JOIN "QingLong3PluginPackageAutomationPublications" AS previous
ON previous.publication_digest =
publication.previous_publication_digest
LEFT JOIN "QingLong3PluginPackageAutomationDispositionEvents"
AS disposition
ON disposition.event_digest = publication.lifecycle_event_digest
LEFT JOIN "QingLong3PluginPackageLifecycleEvents" AS lifecycle
ON lifecycle.event_digest = publication.lifecycle_event_digest
AND disposition.event_kind = 'lifecycle'
LEFT JOIN "QingLong3PluginPackageQuarantineEvents" AS quarantine
ON quarantine.event_digest = publication.lifecycle_event_digest
AND disposition.event_kind = 'quarantine'
WHERE materialized.generation_digest IS NULL
OR materialized.project_id <> publication.project_id
OR materialized.package_name <> publication.package_name
@@ -2129,14 +2154,25 @@ function assertPluginPackageAutomationPublicationIntegrity(
)
OR (
publication.lifecycle_event_digest IS NOT NULL AND (
lifecycle.event_digest IS NULL OR
lifecycle.project_id <> publication.project_id OR
lifecycle.package_name <> publication.package_name OR
lifecycle.installation_id <> publication.installation_id OR
lifecycle.lock_digest <> publication.lock_digest OR
lifecycle.generation_digest <> publication.generation_digest OR
lifecycle.materialized_revision_digest <>
publication.materialized_revision_digest OR
disposition.event_digest IS NULL OR
disposition.event_kind = 'lifecycle' AND (
lifecycle.event_digest IS NULL OR
lifecycle.project_id <> publication.project_id OR
lifecycle.package_name <> publication.package_name OR
lifecycle.installation_id <> publication.installation_id OR
lifecycle.lock_digest <> publication.lock_digest OR
lifecycle.generation_digest <> publication.generation_digest OR
lifecycle.materialized_revision_digest <>
publication.materialized_revision_digest
) OR
disposition.event_kind = 'quarantine' AND (
quarantine.event_digest IS NULL OR
quarantine.project_id <> publication.project_id OR
quarantine.package_name <> publication.package_name OR
quarantine.installation_id <> publication.installation_id OR
quarantine.lock_digest <> publication.lock_digest OR
publication.state <> 'withdrawn'
) OR
previous.installation_id <> publication.installation_id OR
previous.lock_digest <> publication.lock_digest OR
previous.generation_digest <> publication.generation_digest OR
@@ -2496,10 +2532,11 @@ export async function auditLocalSqliteReadiness(
!capability ||
capability.contract_name !== LOCAL_SQLITE_CONTRACT_NAME ||
capability.contract_version !== LOCAL_SQLITE_CONTRACT_VERSION ||
capability.migration_id !== '0087-run-attempt-log-retention' ||
capability.migration_id !==
'0089-plugin-package-automation-disposition-events' ||
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_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_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_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
@@ -4486,6 +4486,24 @@ export const pluginPackageLifecycleTasks = sqliteTable(
],
);
export const pluginPackageAutomationDispositionEvents = sqliteTable(
'QingLong3PluginPackageAutomationDispositionEvents',
{
eventDigest: text('event_digest').primaryKey(),
eventKind: text('event_kind').notNull(),
},
(table) => [
check(
'ql3_plugin_package_automation_disposition_kind_check',
sql`${table.eventKind} in ('lifecycle','quarantine')`,
),
check(
'ql3_plugin_package_automation_disposition_digest_check',
sql`length(${table.eventDigest}) = 64 and ${table.eventDigest} not glob '*[^0-9a-f]*'`,
),
],
);
export const pluginPackageAutomationPublications = sqliteTable(
'QingLong3PluginPackageAutomationPublications',
{
@@ -4510,7 +4528,7 @@ export const pluginPackageAutomationPublications = sqliteTable(
{ onDelete: 'restrict', onUpdate: 'restrict' },
),
lifecycleEventDigest: text('lifecycle_event_digest').references(
() => pluginPackageLifecycleEvents.eventDigest,
() => pluginPackageAutomationDispositionEvents.eventDigest,
{ onDelete: 'restrict', onUpdate: 'restrict' },
),
publishedAtMs: integer('published_at_ms').notNull(),
@@ -4877,6 +4895,7 @@ export const localSqliteSchema = Object.freeze({
pluginPackageLifecycleHeads,
pluginPackageLifecycleReceipts,
pluginPackageLifecycleTasks,
pluginPackageAutomationDispositionEvents,
pluginPackageAutomationPublications,
pluginPackageAutomationPublicationHeads,
pluginPackageWorkflowAdmissions,