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
@@ -293,5 +293,10 @@ export const postgresqlMainMigrationManifest: MigrationStreamManifest =
checksum:
'ab2d0eee3d85a937e1e87243b1fd1e75181529122b64026303488404162e4ba7',
}),
Object.freeze({
id: 'pg-0058-plugin-package-automation-disposition-events',
checksum:
'd184324909f1e450f3c1b58d422796e3869a1360df60c3f2dfe4af0bacc37471',
}),
]),
});
@@ -60,6 +60,7 @@ import { pg0054ApprovalManagementBoundaryMigration } from './pg-0054-approval-ma
import { pg0055RunAttemptLogRetentionMigration } from './pg-0055-run-attempt-log-retention';
import { pg0056RunManagementBoundaryMigration } from '../run-management/pg-0056-run-management-boundary';
import { pg0057RunManagementStopBoundaryMigration } from '../run-management/pg-0057-run-management-stop-boundary';
import { pg0058PluginPackageAutomationDispositionEventsMigration } from './pg-0058-plugin-package-automation-disposition-events';
export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMigrationContext> =
Object.freeze({
@@ -125,5 +126,6 @@ export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMi
pg0055RunAttemptLogRetentionMigration,
pg0056RunManagementBoundaryMigration,
pg0057RunManagementStopBoundaryMigration,
pg0058PluginPackageAutomationDispositionEventsMigration,
]),
});
@@ -0,0 +1,25 @@
import { CAPABILITIES_V56 } from '../run-management/pg-0057-run-management-stop-boundary';
import { definePostgresSqlMigration } from './sqlMigration';
export const CAPABILITIES_V57 = CAPABILITIES_V56.replace(
'"plugin_package_automation_publication":1,',
'"plugin_package_automation_publication":1,"plugin_package_automation_security_withdrawal":1,',
);
export const pg0058PluginPackageAutomationDispositionEventsMigration =
definePostgresSqlMigration({
id: 'pg-0058-plugin-package-automation-disposition-events',
statements: [
`CREATE TABLE "ql3"."plugin_package_automation_disposition_events" (event_digest char(64) PRIMARY KEY, event_kind varchar(16) 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 (event_digest ~ '^[0-9a-f]{64}$'))`,
`INSERT INTO "ql3"."plugin_package_automation_disposition_events" (event_digest, event_kind) SELECT event_digest, 'lifecycle' FROM "ql3"."plugin_package_lifecycle_events"`,
`INSERT INTO "ql3"."plugin_package_automation_disposition_events" (event_digest, event_kind) SELECT event_digest, 'quarantine' FROM "ql3"."plugin_package_quarantine_events" ON CONFLICT (event_digest) DO NOTHING`,
`CREATE FUNCTION "ql3"."register_plugin_package_automation_disposition_event"() RETURNS trigger LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = pg_catalog, ql3 AS $ql3$ DECLARE kind_value varchar(16); BEGIN kind_value := CASE TG_TABLE_NAME WHEN 'plugin_package_lifecycle_events' THEN 'lifecycle' WHEN 'plugin_package_quarantine_events' THEN 'quarantine' ELSE NULL END; IF kind_value IS NULL THEN RAISE EXCEPTION 'unsupported automation disposition source' USING ERRCODE = 'check_violation'; END IF; INSERT INTO "ql3"."plugin_package_automation_disposition_events" (event_digest, event_kind) VALUES (NEW.event_digest, kind_value) ON CONFLICT (event_digest) DO NOTHING; RETURN NEW; END $ql3$`,
`REVOKE ALL ON FUNCTION "ql3"."register_plugin_package_automation_disposition_event"() FROM PUBLIC`,
`CREATE TRIGGER ql3_plugin_package_automation_lifecycle_disposition_insert AFTER INSERT ON "ql3"."plugin_package_lifecycle_events" FOR EACH ROW EXECUTE FUNCTION "ql3"."register_plugin_package_automation_disposition_event"()`,
`CREATE TRIGGER ql3_plugin_package_automation_quarantine_disposition_insert AFTER INSERT ON "ql3"."plugin_package_quarantine_events" FOR EACH ROW EXECUTE FUNCTION "ql3"."register_plugin_package_automation_disposition_event"()`,
`ALTER TABLE "ql3"."plugin_package_automation_publications" DROP CONSTRAINT ql3_plugin_package_automation_publication_lifecycle_fk`,
`ALTER TABLE "ql3"."plugin_package_automation_publications" ADD CONSTRAINT ql3_plugin_package_automation_publication_disposition_fk FOREIGN KEY (lifecycle_event_digest) REFERENCES "ql3"."plugin_package_automation_disposition_events" (event_digest) ON DELETE RESTRICT`,
`REVOKE ALL ON "ql3"."plugin_package_automation_disposition_events" FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager, ql3_package_executor, ql3_worker_ingress`,
`DO $ql3$ BEGIN UPDATE "ql3"."schema_capabilities" SET contract_version = 57, migration_id = 'pg-0058-plugin-package-automation-disposition-events', capabilities = '${CAPABILITIES_V57}'::jsonb, updated_at_ms = floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint WHERE contract_name = 'control-core' AND contract_version = 56 AND migration_id = 'pg-0057-run-management-stop-boundary' AND capabilities = '${CAPABILITIES_V56}'::jsonb; IF NOT FOUND THEN RAISE EXCEPTION 'control-core capability is not at version 56' USING ERRCODE = 'check_violation'; END IF; END $ql3$`,
],
});
@@ -25,6 +25,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,
@@ -56,15 +57,14 @@ import {
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
import { isPostgresAvailabilityError } from '../../connection/pool';
import { PostgresPluginPackageAutomationPublicationRepository } from '../publication/pluginPackageAutomationPublicationRepository';
type Row = Record<string, unknown>;
type Queryable = Pick<PostgresQueryable, 'query'>;
export const CLUSTER_PLUGIN_PACKAGE_QUARANTINE_TARGET_LIMIT = 128;
function unavailable(
cause?: unknown,
): PluginPackageQuarantineUnavailableError {
function unavailable(cause?: unknown): PluginPackageQuarantineUnavailableError {
return new PluginPackageQuarantineUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
@@ -189,8 +189,10 @@ export class PostgresPluginPackageQuarantineRepository
if (result.rows.length !== 1) throw unavailable();
try {
return normalizePluginPackageQuarantineEvent(
recordJson(result.rows[0]!, 'eventJson') as unknown as
PluginPackageQuarantineEvent,
recordJson(
result.rows[0]!,
'eventJson',
) as unknown as PluginPackageQuarantineEvent,
);
} catch (error) {
if (error instanceof PluginPackageQuarantineUnavailableError) {
@@ -215,8 +217,10 @@ export class PostgresPluginPackageQuarantineRepository
if (result.rows.length !== 1) throw unavailable();
try {
const receipt = normalizePluginPackageWithdrawalReceipt(
recordJson(result.rows[0]!, 'receiptJson') as unknown as
PluginPackageWithdrawalReceipt,
recordJson(
result.rows[0]!,
'receiptJson',
) as unknown as PluginPackageWithdrawalReceipt,
);
assertPluginPackageWithdrawalMatchesEvent(event, receipt);
await this.#assertReceiptRelations(queryable, receipt);
@@ -296,8 +300,10 @@ export class PostgresPluginPackageQuarantineRepository
if (snapshots.rows.length !== 1) throw unavailable();
try {
const snapshot = normalizeProjectToolDefinitionSnapshot(
recordJson(snapshots.rows[0]!, 'snapshotJson') as unknown as
ProjectToolDefinitionSnapshot,
recordJson(
snapshots.rows[0]!,
'snapshotJson',
) as unknown as ProjectToolDefinitionSnapshot,
);
if (
snapshot.sources.length !== receipt.capability.retainedSourceCount ||
@@ -316,6 +322,31 @@ export class PostgresPluginPackageQuarantineRepository
}
throw unavailable(error);
}
const automation = await queryable.query<Row>(
`SELECT state,
lifecycle_event_digest AS "lifecycleEventDigest"
FROM "ql3"."plugin_package_automation_publications"
WHERE project_id = $1 AND package_name = $2
AND installation_id = $3 AND lock_digest = $4
ORDER BY version DESC
LIMIT 1`,
[
receipt.target.projectId,
receipt.target.packageName,
receipt.target.installationId,
receipt.target.lockDigest,
],
);
if (automation.rows.length > 1) throw unavailable();
const publication = automation.rows[0];
if (
publication &&
(text(publication, 'state') === 'active' ||
(publication.lifecycleEventDigest === receipt.eventDigest &&
text(publication, 'state') !== 'withdrawn'))
) {
throw unavailable();
}
}
async #findStored(
@@ -345,9 +376,7 @@ export class PostgresPluginPackageQuarantineRepository
LIMIT $2`,
[lockDigest, CLUSTER_PLUGIN_PACKAGE_QUARANTINE_TARGET_LIMIT + 1],
);
if (
result.rows.length > CLUSTER_PLUGIN_PACKAGE_QUARANTINE_TARGET_LIMIT
) {
if (result.rows.length > CLUSTER_PLUGIN_PACKAGE_QUARANTINE_TARGET_LIMIT) {
throw new PluginPackageQuarantineConflictError(
'matching install targets exceed the Cluster limit',
);
@@ -355,8 +384,10 @@ export class PostgresPluginPackageQuarantineRepository
return Object.freeze(
result.rows.map((row) => {
const record = normalizePluginPackageInstallRecord(
recordJson(row, 'recordJson') as unknown as
PluginPackageInstallRecord,
recordJson(
row,
'recordJson',
) as unknown as PluginPackageInstallRecord,
);
return Object.freeze({
projectId: record.projectId,
@@ -411,8 +442,10 @@ export class PostgresPluginPackageQuarantineRepository
let record: Readonly<PluginPackageInstallRecord>;
try {
record = normalizePluginPackageInstallRecord(
recordJson(result.rows[0]!, 'recordJson') as unknown as
PluginPackageInstallRecord,
recordJson(
result.rows[0]!,
'recordJson',
) as unknown as PluginPackageInstallRecord,
);
} catch (error) {
if (error instanceof PluginPackageQuarantineUnavailableError) {
@@ -436,12 +469,45 @@ export class PostgresPluginPackageQuarantineRepository
return record;
}
async #withdrawAutomation(
client: PostgresClient,
event: Readonly<PluginPackageQuarantineEvent>,
record: Readonly<PluginPackageInstallRecord>,
committedAtMs: number,
): Promise<void> {
const publications =
new PostgresPluginPackageAutomationPublicationRepository(this.pool);
const current = await publications.findCurrentInTransaction(
client,
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;
await publications.publishSecurityWithdrawalInTransaction(
client,
createPluginPackageAutomationLifecyclePublication({
previous: current,
state: 'withdrawn',
lifecycleEventDigest: event.eventDigest,
publishedAtMs: committedAtMs,
}),
);
}
async #activeContributions(
queryable: Queryable,
projectId: string,
): Promise<
readonly Readonly<ProjectToolDefinitionSnapshotContribution>[]
> {
): Promise<readonly Readonly<ProjectToolDefinitionSnapshotContribution>[]> {
const result = await queryable.query<Row>(
`SELECT revision.revision_json AS "revisionJson"
FROM "ql3"."plugin_package_install_heads" AS head
@@ -532,9 +598,7 @@ export class PostgresPluginPackageQuarantineRepository
MAX_PLUGIN_PACKAGE_QUARANTINE_TASK_WITHDRAWALS + 1,
],
);
if (
result.rows.length > MAX_PLUGIN_PACKAGE_QUARANTINE_TASK_WITHDRAWALS
) {
if (result.rows.length > MAX_PLUGIN_PACKAGE_QUARANTINE_TASK_WITHDRAWALS) {
throw new PluginPackageQuarantineConflictError(
'owned Tasks exceed the quarantine withdrawal limit',
);
@@ -608,14 +672,11 @@ export class PostgresPluginPackageQuarantineRepository
'event digest is bound to another quarantine',
);
}
const existingReceipt = await this.#receiptByEvent(
client,
existingEvent,
);
const existingReceipt = await this.#receiptByEvent(client, existingEvent);
if (!existingReceipt) throw unavailable();
return Object.freeze({ created: false, receipt: existingReceipt });
}
await this.#install(client, event);
const install = await this.#install(client, event);
const committedAtMs = Math.max(
await this.#databaseNowMs(client),
event.occurredAtMs,
@@ -706,6 +767,9 @@ export class PostgresPluginPackageQuarantineRepository
) {
throw unavailable();
}
if (event.target.installState === 'active') {
await this.#withdrawAutomation(client, event, install, committedAtMs);
}
const stored = await this.#findStored(client, event.eventDigest);
if (!stored || !same(stored, receipt)) throw unavailable();
return Object.freeze({
@@ -1,8 +1,5 @@
// PostgreSQL adapter owned by Plugin Package publication and recovery.
import type {
PostgresClient,
PostgresPool,
} from '@qinglong/runtime-core';
import type { PostgresClient, PostgresPool } from '@qinglong/runtime-core';
import {
InvalidPluginPackageAutomationPublicationError,
MAX_PLUGIN_PACKAGE_AUTOMATION_PUBLICATION_BYTES,
@@ -49,7 +46,10 @@ function unavailable(
});
}
function targetIdentity(projectId: unknown, packageName: unknown): {
function targetIdentity(
projectId: unknown,
packageName: unknown,
): {
readonly projectId: string;
readonly packageName: string;
} {
@@ -147,8 +147,7 @@ export class PostgresPluginPackageAutomationPublicationRepository
postgresRequiredString(row.generationDigest, unavailable) ||
publication.target.materializedRevisionDigest !==
postgresRequiredString(row.materializedRevisionDigest, unavailable) ||
publication.state !==
postgresRequiredString(row.state, unavailable) ||
publication.state !== postgresRequiredString(row.state, unavailable) ||
publication.version !==
postgresRequiredInteger(row.version, unavailable) ||
publication.publishedAtMs !==
@@ -158,9 +157,7 @@ export class PostgresPluginPackageAutomationPublicationRepository
}
return publication;
} catch (error) {
if (
error instanceof PluginPackageAutomationPublicationUnavailableError
) {
if (error instanceof PluginPackageAutomationPublicationUnavailableError) {
throw error;
}
throw unavailable(error);
@@ -393,9 +390,10 @@ export class PostgresPluginPackageAutomationPublicationRepository
return this.#findCurrent(client, projectId, packageName, true);
}
async publishInTransaction(
async #publishInTransaction(
client: PostgresClient,
value: Readonly<PluginPackageAutomationPublication>,
securityWithdrawal: boolean,
): Promise<
Readonly<{
status: 'created' | 'existing';
@@ -443,10 +441,7 @@ export class PostgresPluginPackageAutomationPublicationRepository
);
}
try {
assertPluginPackageAutomationPublicationSuccessor(
current,
publication,
);
assertPluginPackageAutomationPublicationSuccessor(current, publication);
} catch (error) {
if (error instanceof InvalidPluginPackageAutomationPublicationError) {
throw new PluginPackageAutomationPublicationConflictError(
@@ -469,20 +464,14 @@ export class PostgresPluginPackageAutomationPublicationRepository
);
if (
revision.rows.length !== 1 ||
postgresRequiredString(
revision.rows[0]!.revisionDigest,
unavailable,
) !== publication.target.materializedRevisionDigest ||
postgresRequiredString(revision.rows[0]!.revisionDigest, unavailable) !==
publication.target.materializedRevisionDigest ||
postgresRequiredString(revision.rows[0]!.projectId, unavailable) !==
publication.target.projectId ||
postgresRequiredString(
revision.rows[0]!.packageName,
unavailable,
) !== publication.target.packageName ||
postgresRequiredInteger(
revision.rows[0]!.generation,
unavailable,
) !== publication.target.generation ||
postgresRequiredString(revision.rows[0]!.packageName, unavailable) !==
publication.target.packageName ||
postgresRequiredInteger(revision.rows[0]!.generation, unavailable) !==
publication.target.generation ||
postgresRequiredString(revision.rows[0]!.lockDigest, unavailable) !==
publication.target.lockDigest
) {
@@ -490,41 +479,43 @@ export class PostgresPluginPackageAutomationPublicationRepository
'materialized revision fence does not match publication target',
);
}
const securityFence = await client.query<Row>(
`SELECT
EXISTS (
SELECT 1
FROM "ql3"."plugin_package_quarantine_events" AS quarantine
WHERE quarantine.project_id = $1
AND quarantine.package_name = $2
AND quarantine.installation_id = $3
AND quarantine.lock_digest = $4
) OR EXISTS (
SELECT 1
FROM "ql3"."plugin_package_publisher_provenance" AS provenance
JOIN "ql3"."plugin_package_publisher_revocation_receipts" AS revoked
ON revoked.publisher = provenance.publisher
AND revoked.key_id = provenance.key_id
WHERE provenance.installation_id = $3
AND provenance.lock_digest = $4
) AS "blocked"`,
[
publication.target.projectId,
publication.target.packageName,
publication.target.installationId,
publication.target.lockDigest,
],
);
if (
securityFence.rows.length !== 1 ||
typeof securityFence.rows[0]?.blocked !== 'boolean'
) {
throw unavailable();
}
if (securityFence.rows[0].blocked) {
throw new PluginPackageAutomationPublicationConflictError(
'security-fenced Package generation cannot publish automation',
if (!securityWithdrawal) {
const securityFence = await client.query<Row>(
`SELECT
EXISTS (
SELECT 1
FROM "ql3"."plugin_package_quarantine_events" AS quarantine
WHERE quarantine.project_id = $1
AND quarantine.package_name = $2
AND quarantine.installation_id = $3
AND quarantine.lock_digest = $4
) OR EXISTS (
SELECT 1
FROM "ql3"."plugin_package_publisher_provenance" AS provenance
JOIN "ql3"."plugin_package_publisher_revocation_receipts" AS revoked
ON revoked.publisher = provenance.publisher
AND revoked.key_id = provenance.key_id
WHERE provenance.installation_id = $3
AND provenance.lock_digest = $4
) AS "blocked"`,
[
publication.target.projectId,
publication.target.packageName,
publication.target.installationId,
publication.target.lockDigest,
],
);
if (
securityFence.rows.length !== 1 ||
typeof securityFence.rows[0]?.blocked !== 'boolean'
) {
throw unavailable();
}
if (securityFence.rows[0].blocked) {
throw new PluginPackageAutomationPublicationConflictError(
'security-fenced Package generation cannot publish automation',
);
}
}
await client.query(
`INSERT INTO "ql3"."plugin_package_automation_publications" (
@@ -601,13 +592,41 @@ export class PostgresPluginPackageAutomationPublicationRepository
});
}
async publish(
publishInTransaction(
client: PostgresClient,
value: Readonly<PluginPackageAutomationPublication>,
): Promise<
Readonly<{
status: 'created' | 'existing';
publication: Readonly<PluginPackageAutomationPublication>;
}>
> {
return this.#publishInTransaction(client, value, false);
}
publishSecurityWithdrawalInTransaction(
client: PostgresClient,
value: Readonly<PluginPackageAutomationPublication>,
): Promise<
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(client, publication, true);
}
async publish(value: Readonly<PluginPackageAutomationPublication>): Promise<
Readonly<{
status: 'created' | 'existing';
publication: Readonly<PluginPackageAutomationPublication>;
}>
> {
for (
let attempt = 0;
@@ -1707,6 +1707,24 @@ export const pluginPackageLifecyclePlans = ql3Schema.table(
],
);
export const pluginPackageAutomationDispositionEvents = ql3Schema.table(
'plugin_package_automation_disposition_events',
{
eventDigest: char('event_digest', { length: 64 }).primaryKey(),
eventKind: varchar('event_kind', { length: 16 }).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`${table.eventDigest} ~ '^[0-9a-f]{64}$'`,
),
],
);
export const pluginPackageAutomationPublications = ql3Schema.table(
'plugin_package_automation_publications',
{
@@ -1743,9 +1761,9 @@ export const pluginPackageAutomationPublications = ql3Schema.table(
foreignColumns: [table.publicationDigest],
}).onDelete('restrict'),
foreignKey({
name: 'ql3_plugin_package_automation_publication_lifecycle_fk',
name: 'ql3_plugin_package_automation_publication_disposition_fk',
columns: [table.lifecycleEventDigest],
foreignColumns: [pluginPackageLifecycleEvents.eventDigest],
foreignColumns: [pluginPackageAutomationDispositionEvents.eventDigest],
}).onDelete('restrict'),
uniqueIndex('ql3_plugin_package_automation_publication_version_key').on(
table.projectId,
@@ -5653,9 +5671,11 @@ export const pluginPackageWorkflowTaskAttemptAdmissions = ql3Schema.table(
uniqueIndex(
'plugin_package_workflow_task_attempt_admissions_event_id_key',
).on(table.eventId),
uniqueIndex(
'plugin_package_workflow_task_attempt_admissions_epoch_key',
).on(table.runId, table.stepRunId, table.stepRunVersion),
uniqueIndex('plugin_package_workflow_task_attempt_admissions_epoch_key').on(
table.runId,
table.stepRunId,
table.stepRunVersion,
),
uniqueIndex(
'plugin_package_workflow_task_attempt_admissions_number_key',
).on(table.runId, table.attemptNumber),
@@ -5684,10 +5704,7 @@ export const pluginPackageWorkflowTaskAttemptAdmissions = ql3Schema.table(
}).onDelete('restrict'),
foreignKey({
name: 'ql3_pp_workflow_task_attempt_reconciliation_fk',
columns: [
table.generationDigest,
table.taskReconciliationReceiptDigest,
],
columns: [table.generationDigest, table.taskReconciliationReceiptDigest],
foreignColumns: [
pluginPackageTaskReconciliations.generationDigest,
pluginPackageTaskReconciliations.receiptDigest,
@@ -5724,9 +5741,11 @@ export const pluginPackageWorkflowTaskAttemptAdmissions = ql3Schema.table(
'ql3_plugin_package_workflow_task_attempt_admission_json_check',
sql`jsonb_typeof(${table.receiptJson}) = 'object' and octet_length(${table.receiptJson}::text) between 2 and 16384 and ${table.receiptJson} @> jsonb_build_object('schema', 'qinglong/plugin-package-workflow-task-attempt-admission@v1', 'receiptDigest', ${table.receiptDigest}, 'attemptId', ${table.attemptId}, 'planDigest', ${table.planDigest}, 'runId', ${table.runId}, 'stepRunId', ${table.stepRunId}, 'stepRunVersion', ${table.stepRunVersion}, 'stepRunDigest', ${table.stepRunDigest}, 'resourceTaskId', ${table.resourceTaskId}, 'taskReconciliationReceiptDigest', ${table.taskReconciliationReceiptDigest}, 'taskId', ${table.taskId}, 'taskRevision', ${table.taskRevision}, 'taskDefinitionDigest', ${table.taskDefinitionDigest}, 'executorType', ${table.executorType}, 'executionDigest', ${table.executionDigest}, 'attemptNumber', ${table.attemptNumber}, 'eventId', ${table.eventId}, 'runVersion', ${table.runVersion}, 'runEventSequence', ${table.runEventSequence}, 'admittedAtMs', ${table.admittedAtMs})`,
),
index(
'ql3_pp_workflow_task_attempt_candidate_idx',
).on(table.runId, table.stepRunId, table.admittedAtMs),
index('ql3_pp_workflow_task_attempt_candidate_idx').on(
table.runId,
table.stepRunId,
table.admittedAtMs,
),
],
);
@@ -5803,6 +5822,7 @@ export const ql3PostgresTables = [
pluginPackageLifecycleReceipts,
pluginPackageLifecycleTasks,
pluginPackageLifecyclePlans,
pluginPackageAutomationDispositionEvents,
pluginPackageAutomationPublications,
pluginPackageAutomationPublicationHeads,
pluginPackageWorkflowAdmissions,
@@ -15,8 +15,8 @@ export interface PostgresSchemaContractFunction {
export interface PostgresSchemaContract {
readonly schema: 'ql3';
readonly contractName: 'control-core';
readonly contractVersion: 56;
readonly migrationId: 'pg-0057-run-management-stop-boundary';
readonly contractVersion: 57;
readonly migrationId: 'pg-0058-plugin-package-automation-disposition-events';
readonly minimumServerMajor: 16;
readonly maximumServerMajor: 18;
readonly capabilities: Readonly<{
@@ -44,6 +44,7 @@ export interface PostgresSchemaContract {
plugin_package_admission: 1;
plugin_package_authority_split: 1;
plugin_package_automation_publication: 1;
plugin_package_automation_security_withdrawal: 1;
plugin_package_automation_start_guard: 1;
plugin_package_workflow_admission: 1;
plugin_package_workflow_run_list: 1;
@@ -103,8 +104,8 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
Object.freeze({
schema: 'ql3',
contractName: 'control-core',
contractVersion: 56,
migrationId: 'pg-0057-run-management-stop-boundary',
contractVersion: 57,
migrationId: 'pg-0058-plugin-package-automation-disposition-events',
minimumServerMajor: 16,
maximumServerMajor: 18,
capabilities: Object.freeze({
@@ -125,6 +126,7 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
plugin_package_admission: 1,
plugin_package_authority_split: 1,
plugin_package_automation_publication: 1,
plugin_package_automation_security_withdrawal: 1,
plugin_package_automation_start_guard: 1,
plugin_package_workflow_admission: 1,
plugin_package_workflow_run_list: 1,
@@ -382,6 +384,10 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
'expires_at_ms',
'plan_json',
]),
table('plugin_package_automation_disposition_events', [
'event_digest',
'event_kind',
]),
table('plugin_package_automation_publications', [
'publication_digest',
'project_id',
@@ -1445,6 +1451,7 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
'ql3_plugin_package_lifecycle_plan_impact_key',
'ql3_plugin_package_lifecycle_plan_expiry_idx',
'plugin_package_automation_publications_pkey',
'plugin_package_automation_disposition_events_pkey',
'ql3_plugin_package_automation_publication_version_key',
'ql3_plugin_package_automation_publication_previous_key',
'ql3_plugin_package_automation_publication_generation_idx',
@@ -1736,6 +1743,8 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
'ql3_plugin_package_automation_publication_digest_check',
'ql3_plugin_package_automation_publication_json_check',
'ql3_plugin_package_automation_publication_head_state_check',
'ql3_plugin_package_automation_disposition_kind_check',
'ql3_plugin_package_automation_disposition_digest_check',
'ql3_plugin_package_workflow_admission_identity_check',
'ql3_plugin_package_workflow_admission_digest_check',
'ql3_plugin_package_workflow_admission_json_check',
@@ -2175,7 +2184,7 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
'ql3_plugin_package_lifecycle_plan_install_fk',
'ql3_plugin_package_automation_publication_revision_fk',
'ql3_plugin_package_automation_publication_previous_fk',
'ql3_plugin_package_automation_publication_lifecycle_fk',
'ql3_plugin_package_automation_publication_disposition_fk',
'ql3_plugin_package_automation_publication_head_publication_fk',
'ql3_plugin_package_workflow_admission_run_fk',
'ql3_plugin_package_workflow_admission_publication_fk',
@@ -2302,6 +2311,14 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
'ql3_run_retry_policies_run_fk',
]),
functions: Object.freeze([
Object.freeze({
name: 'register_plugin_package_automation_disposition_event',
identityArguments: '',
owner: 'ql3_migration',
securityDefiner: true,
volatility: 'volatile',
configuration: Object.freeze(['search_path=pg_catalog, ql3']),
}),
Object.freeze({
name: 'enforce_plugin_package_stage_provenance',
identityArguments: '',
@@ -233,6 +233,12 @@ const REQUIRED_RUNTIME_PRIVILEGES = Object.freeze({
update: false,
delete: false,
}),
plugin_package_automation_disposition_events: Object.freeze({
select: false,
insert: false,
update: false,
delete: false,
}),
plugin_package_automation_publication_heads: Object.freeze({
select: true,
insert: false,
@@ -740,6 +746,12 @@ const REQUIRED_ADMIN_PRIVILEGES = Object.freeze({
update: false,
delete: false,
}),
plugin_package_automation_disposition_events: Object.freeze({
select: false,
insert: false,
update: false,
delete: false,
}),
plugin_package_automation_publication_heads: Object.freeze({
select: false,
insert: false,
@@ -1492,6 +1504,7 @@ const REQUIRED_RUNTIME_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges =
plugin_package_lifecycle_blocking_runs: false,
plugin_package_run_start_allowed: true,
plugin_package_tool_start_allowed: true,
register_plugin_package_automation_disposition_event: false,
});
const REQUIRED_PACKAGE_MANAGER_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges =
@@ -1509,6 +1522,7 @@ const REQUIRED_PACKAGE_MANAGER_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges =
plugin_package_lifecycle_blocking_runs: false,
plugin_package_run_start_allowed: false,
plugin_package_tool_start_allowed: false,
register_plugin_package_automation_disposition_event: false,
});
const REQUIRED_PACKAGE_EXECUTOR_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges =
@@ -1526,6 +1540,7 @@ const REQUIRED_PACKAGE_EXECUTOR_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges
plugin_package_lifecycle_blocking_runs: true,
plugin_package_run_start_allowed: false,
plugin_package_tool_start_allowed: false,
register_plugin_package_automation_disposition_event: false,
});
const REQUIRED_WORKER_CREDENTIAL_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges =
@@ -108,6 +108,7 @@ test('defines the immutable PostgreSQL capability and Run core stream', async ()
'pg-0055-run-attempt-log-retention',
'pg-0056-run-management-boundary',
'pg-0057-run-management-stop-boundary',
'pg-0058-plugin-package-automation-disposition-events',
],
);
for (const migration of postgresqlMainMigrationStream.migrations) {
@@ -528,6 +529,11 @@ test('freezes every published PostgreSQL migration checksum', () => {
checksum:
'ab2d0eee3d85a937e1e87243b1fd1e75181529122b64026303488404162e4ba7',
},
{
id: 'pg-0058-plugin-package-automation-disposition-events',
checksum:
'd184324909f1e450f3c1b58d422796e3869a1360df60c3f2dfe4af0bacc37471',
},
];
assert.deepEqual(
postgresqlMainMigrationStream.migrations.map(({ id, checksum }) => ({
@@ -85,6 +85,7 @@ function validPrivileges() {
plugin_package_lifecycle_tasks: [false, false, false, false],
plugin_package_lifecycle_plans: [false, false, false, false],
plugin_package_automation_publications: [true, false, false, false],
plugin_package_automation_disposition_events: [false, false, false, false],
plugin_package_automation_publication_heads: [true, false, false, false],
plugin_package_workflow_admissions: [true, true, false, false],
plugin_package_workflow_admission_steps: [true, true, false, false],
@@ -202,6 +203,7 @@ function validAdminPrivileges() {
plugin_package_lifecycle_tasks: [false, false, false, false],
plugin_package_lifecycle_plans: [false, false, false, false],
plugin_package_automation_publications: [false, false, false, false],
plugin_package_automation_disposition_events: [false, false, false, false],
plugin_package_automation_publication_heads: [false, false, false, false],
plugin_package_workflow_admissions: [false, false, false, false],
plugin_package_workflow_admission_steps: [false, false, false, false],
@@ -748,7 +750,7 @@ test('accepts the exact PostgreSQL control schema and least-privilege runtime ro
serverMajor: 16,
currentUser: 'ql3_runtime',
contractName: 'control-core',
contractVersion: 56,
contractVersion: 57,
migrationIds: [
'pg-0001-schema-capability',
'pg-0002-run-core',
@@ -807,6 +809,7 @@ test('accepts the exact PostgreSQL control schema and least-privilege runtime ro
'pg-0055-run-attempt-log-retention',
'pg-0056-run-management-boundary',
'pg-0057-run-management-stop-boundary',
'pg-0058-plugin-package-automation-disposition-events',
],
});
});
@@ -837,10 +840,10 @@ test('accepts the exact schema and isolated least-privilege admin role', async (
}),
);
assert.equal(report.currentUser, 'ql3_admin');
assert.equal(report.contractVersion, 56);
assert.equal(report.contractVersion, 57);
assert.equal(
report.migrationIds.at(-1),
'pg-0057-run-management-stop-boundary',
'pg-0058-plugin-package-automation-disposition-events',
);
});
@@ -853,10 +856,10 @@ test('accepts the isolated least-privilege automation manager role', async () =>
}),
);
assert.equal(report.currentUser, 'ql3_automation_manager');
assert.equal(report.contractVersion, 56);
assert.equal(report.contractVersion, 57);
assert.equal(
report.migrationIds.at(-1),
'pg-0057-run-management-stop-boundary',
'pg-0058-plugin-package-automation-disposition-events',
);
const widened = automationManagerPrivileges();
@@ -885,10 +888,10 @@ test('accepts the isolated least-privilege human Approval manager role', async (
}),
);
assert.equal(report.currentUser, 'ql3_approval_manager');
assert.equal(report.contractVersion, 56);
assert.equal(report.contractVersion, 57);
assert.equal(
report.migrationIds.at(-1),
'pg-0057-run-management-stop-boundary',
'pg-0058-plugin-package-automation-disposition-events',
);
const widened = approvalManagerPrivileges();
@@ -919,10 +922,10 @@ test('accepts the isolated least-privilege Run manager role', async () => {
}),
);
assert.equal(report.currentUser, 'ql3_run_manager');
assert.equal(report.contractVersion, 56);
assert.equal(report.contractVersion, 57);
assert.equal(
report.migrationIds.at(-1),
'pg-0057-run-management-stop-boundary',
'pg-0058-plugin-package-automation-disposition-events',
);
const widened = runManagerPrivileges();
@@ -1054,10 +1057,10 @@ test('accepts the exact schema and isolated Worker ingress role', async () => {
}),
);
assert.equal(report.currentUser, 'ql3_worker_ingress');
assert.equal(report.contractVersion, 56);
assert.equal(report.contractVersion, 57);
assert.equal(
report.migrationIds.at(-1),
'pg-0057-run-management-stop-boundary',
'pg-0058-plugin-package-automation-disposition-events',
);
});