feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
@@ -0,0 +1,33 @@
export {
PgPoolBinding,
createPostgresDatabaseOpener,
isPostgresTlsDnsServername,
type OpenPostgresDatabaseOptions,
type PostgresConnectionOptions,
type PostgresDatabaseRole,
type PostgresPoolOptions,
} from '../connection/pool';
export {
PostgresConnectionEnvironmentError,
loadPostgresConnectionEnvironment,
type PostgresConnectionEnvironment,
type PostgresConnectionEnvironmentKeys,
} from '../connection/connectionEnvironment';
export {
loadPostgresCertificateAuthorityFile,
type PostgresCertificateAuthorityFileInspection,
} from '../connection/certificateAuthority';
export {
PostgresSchemaReadinessError,
assertPostgresApprovalManagerSchemaReady,
type PostgresSchemaReadinessReport,
} from '../schema/schemaReadiness';
export { postgresqlMainMigrationManifest } from '../migration/migrationManifest';
export { PostgresApprovalRequestRepository } from '../approved-action/approvalRequestRepository';
export { PostgresApprovalRequestSource } from '../approved-action/approvalRequestSource';
export { PostgresProjectPolicyRepository } from '../security/projectPolicyRepository';
export { PostgresSecurityAuditRepository } from '../security/securityAuditRepository';
export {
PostgresPluginPackageIdentityKeysetLedgerRepository as PostgresApprovalManagementIdentityKeysetLedgerRepository,
type ClusterManagementIdentityAuthority,
} from '../management/pluginPackageIdentityKeysetLedgerRepository';
@@ -0,0 +1,654 @@
import type {
PostgresClient,
PostgresPool,
} from '@qinglong/runtime-core';
import {
ApprovalMutationConflictError,
ApprovalPolicyFenceConflictError,
ApprovalRequestNotFoundError,
ApprovalRequestStateConflictError,
ApprovalUnavailableError,
approvalRequestDigest,
approvedActionDispatchDigest,
consumeApprovalRequest,
decideApprovalRequest,
normalizeApprovalRequestRecord,
normalizeApprovedActionDispatchRecord,
type ApprovalRequestRecord,
type ApprovalRequestRepository,
type ApprovedActionDispatchRecord,
type ConsumeDurableApprovalRequestCommand,
type ConsumeDurableApprovalRequestResult,
type CreateApprovalRequestCommand,
type CreateApprovalRequestResult,
type DecideApprovalRequestResult,
type DecideDurableApprovalRequestCommand,
} from '@qinglong/runtime-core/approved-action';
import type {
SecurityPolicyFence,
SecuritySubject,
} from '@qinglong/runtime-core/security';
import {
normalizeSecurityAuditRecord,
type SecurityAuditRecord,
} from '@qinglong/runtime-core/security-audit';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredBoolean,
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../repository/definitionRepositorySupport';
import {
findPostgresApprovedActionExecution,
insertPostgresApprovedActionExecutionBaseline,
} from './approvedActionExecutionRepository';
import { createApprovedActionExecution } from '@qinglong/runtime-core/approved-action-execution';
type Row = Record<string, unknown>;
type Queryable = Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>;
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
function unavailable(): ApprovalUnavailableError {
return new ApprovalUnavailableError();
}
function nullableString(value: unknown): string | null {
if (value === null) return null;
return postgresRequiredString(value, unavailable);
}
function nullableInteger(value: unknown): number | null {
if (value === null) return null;
return postgresRequiredInteger(value, unavailable);
}
function same(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function parseRequest(row: Row): Readonly<ApprovalRequestRecord> {
try {
const request = normalizeApprovalRequestRecord(
postgresRequiredJsonObject(
row.requestJson,
unavailable,
) as unknown as ApprovalRequestRecord,
);
if (
approvalRequestDigest(request) !==
postgresRequiredString(row.requestDigest, unavailable)
) {
throw unavailable();
}
return request;
} catch (error) {
if (error instanceof ApprovalUnavailableError) throw error;
throw unavailable();
}
}
function parseDispatch(row: Row): Readonly<ApprovedActionDispatchRecord> {
try {
const dispatch = normalizeApprovedActionDispatchRecord(
postgresRequiredJsonObject(
row.dispatchJson,
unavailable,
) as unknown as ApprovedActionDispatchRecord,
);
if (
approvedActionDispatchDigest(dispatch) !==
postgresRequiredString(row.dispatchDigest, unavailable)
) {
throw unavailable();
}
return dispatch;
} catch (error) {
if (error instanceof ApprovalUnavailableError) throw error;
throw unavailable();
}
}
function parseAudit(row: Row): Readonly<SecurityAuditRecord> {
try {
const subjectType = nullableString(row.subjectType);
const subjectId = nullableString(row.subjectId);
const fenceProjectVersion = nullableInteger(row.fenceProjectVersion);
const reasons = row.reasonsJson;
if (!Array.isArray(reasons)) throw unavailable();
return normalizeSecurityAuditRecord({
eventId: postgresRequiredString(row.eventId, unavailable),
requestId: postgresRequiredString(row.requestId, unavailable),
operationId: postgresRequiredString(row.operationId, unavailable),
projectId: nullableString(row.projectId),
subject:
subjectType === null || subjectId === null
? null
: {
type: subjectType as SecuritySubject['type'],
id: subjectId,
},
authenticationId: nullableString(row.authenticationId),
outcome: postgresRequiredString(
row.outcome,
unavailable,
) as SecurityAuditRecord['outcome'],
reasons: reasons as readonly string[],
fence:
fenceProjectVersion === null
? null
: {
projectVersion: fenceProjectVersion,
bindingVersion: nullableInteger(row.fenceBindingVersion),
},
occurredAtMs: postgresRequiredInteger(row.occurredAtMs, unavailable),
});
} catch (error) {
if (error instanceof ApprovalUnavailableError) throw error;
throw unavailable();
}
}
function mappedError(error: unknown): Error {
if (
error instanceof ApprovalMutationConflictError ||
error instanceof ApprovalPolicyFenceConflictError ||
error instanceof ApprovalRequestNotFoundError ||
error instanceof ApprovalRequestStateConflictError ||
error instanceof ApprovalUnavailableError ||
(error instanceof Error &&
error.name.startsWith('Approval') &&
'code' in error)
) {
return error;
}
const state = postgresSqlState(error);
if (state === '23503' || state === '23505' || state === '23514') {
return new ApprovalMutationConflictError();
}
return new ApprovalUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
function auditMatches(
audit: Readonly<SecurityAuditRecord>,
expected: Readonly<{
operationId: string;
projectId: string;
subject: Readonly<SecuritySubject>;
authenticationId?: string;
outcome: SecurityAuditRecord['outcome'];
fence: Readonly<SecurityPolicyFence>;
}>,
): boolean {
return (
audit.operationId === expected.operationId &&
audit.projectId === expected.projectId &&
audit.subject?.type === expected.subject.type &&
audit.subject.id === expected.subject.id &&
(expected.authenticationId === undefined ||
audit.authenticationId === expected.authenticationId) &&
audit.outcome === expected.outcome &&
audit.fence?.projectVersion === expected.fence.projectVersion &&
audit.fence.bindingVersion === expected.fence.bindingVersion
);
}
async function requestById(
queryable: Queryable,
id: string,
): Promise<Readonly<ApprovalRequestRecord> | null> {
const result = await queryable.query<Row>(
`SELECT request_json AS "requestJson",
request_digest AS "requestDigest"
FROM "ql3"."approval_requests"
WHERE request_id = $1
LIMIT 2`,
[id],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return parseRequest(result.rows[0]!);
}
async function dispatchById(
queryable: Queryable,
id: string,
): Promise<Readonly<ApprovedActionDispatchRecord> | null> {
const result = await queryable.query<Row>(
`SELECT dispatch_json AS "dispatchJson",
dispatch_digest AS "dispatchDigest"
FROM "ql3"."approved_action_dispatches"
WHERE dispatch_id = $1
LIMIT 2`,
[id],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return parseDispatch(result.rows[0]!);
}
async function auditById(
queryable: Queryable,
id: string,
): Promise<Readonly<SecurityAuditRecord> | null> {
const result = await queryable.query<Row>(
`SELECT event_id AS "eventId", request_id AS "requestId",
operation_id AS "operationId", project_id AS "projectId",
subject_type AS "subjectType", subject_id AS "subjectId",
authentication_id AS "authenticationId", outcome AS "outcome",
reasons AS "reasonsJson",
project_version AS "fenceProjectVersion",
binding_version AS "fenceBindingVersion",
occurred_at_ms AS "occurredAtMs"
FROM "ql3"."security_audit_events"
WHERE event_id = $1
LIMIT 2`,
[id],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return parseAudit(result.rows[0]!);
}
async function assertFence(
client: PostgresClient,
projectId: string,
subject: Readonly<SecuritySubject>,
fence: Readonly<SecurityPolicyFence>,
): Promise<void> {
const result = await client.query<Row>(
`SELECT "ql3"."lock_approval_policy_fence"(
$1::varchar, $2::varchar, $3::varchar, $4::integer, $5::integer
) AS "matches"`,
[
projectId,
subject.type,
subject.id,
fence.projectVersion,
fence.bindingVersion,
],
);
if (
result.rows.length !== 1 ||
!postgresRequiredBoolean(result.rows[0]!.matches, unavailable)
) {
throw new ApprovalPolicyFenceConflictError();
}
}
async function insertAudit(
client: PostgresClient,
value: SecurityAuditRecord,
): Promise<void> {
const audit = normalizeSecurityAuditRecord(value);
const result = await client.query(
`INSERT INTO "ql3"."security_audit_events" (
event_id, request_id, operation_id, project_id, subject_type,
subject_id, authentication_id, outcome, reasons,
project_version, binding_version, occurred_at_ms
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11, $12
)`,
[
audit.eventId,
audit.requestId,
audit.operationId,
audit.projectId,
audit.subject?.type ?? null,
audit.subject?.id ?? null,
audit.authenticationId,
audit.outcome,
JSON.stringify(audit.reasons),
audit.fence?.projectVersion ?? null,
audit.fence?.bindingVersion ?? null,
audit.occurredAtMs,
],
);
if (result.rowCount !== 1) throw unavailable();
}
async function insertRequest(
client: PostgresClient,
request: Readonly<ApprovalRequestRecord>,
): Promise<void> {
const result = await client.query(
`INSERT INTO "ql3"."approval_requests" (
request_id, project_id, version, state, action_type, action_ref,
action_digest, preview_digest, requested_by_type, requested_by_id,
decision_id, consumption_id, dispatch_id, expires_at_ms, request_json,
request_digest, updated_at_ms
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14,
$15::jsonb, $16, $17
)`,
[
request.id,
request.projectId,
request.version,
request.state,
request.action.actionType,
request.action.actionRef,
request.action.actionDigest,
request.action.previewDigest,
request.requestedBy.type,
request.requestedBy.id,
request.decisionId,
request.consumptionId,
request.dispatchId,
request.expiresAtMs,
JSON.stringify(request),
approvalRequestDigest(request),
request.consumedAtMs ?? request.decidedAtMs ?? request.requestedAtMs,
],
);
if (result.rowCount !== 1) throw unavailable();
}
async function updateRequest(
client: PostgresClient,
request: Readonly<ApprovalRequestRecord>,
expectedVersion: number,
): Promise<void> {
const result = await client.query(
`UPDATE "ql3"."approval_requests"
SET version = $1, state = $2, decision_id = $3, consumption_id = $4,
dispatch_id = $5, request_json = $6::jsonb, request_digest = $7,
updated_at_ms = $8
WHERE request_id = $9 AND version = $10`,
[
request.version,
request.state,
request.decisionId,
request.consumptionId,
request.dispatchId,
JSON.stringify(request),
approvalRequestDigest(request),
request.consumedAtMs ?? request.decidedAtMs ?? request.requestedAtMs,
request.id,
expectedVersion,
],
);
if (result.rowCount !== 1) throw new ApprovalRequestStateConflictError();
}
async function insertDispatch(
client: PostgresClient,
dispatch: Readonly<ApprovedActionDispatchRecord>,
): Promise<void> {
const result = await client.query(
`INSERT INTO "ql3"."approved_action_dispatches" (
dispatch_id, approval_request_id, project_id, action_type, action_ref,
action_digest, preview_digest, dispatch_json, dispatch_digest,
created_at_ms
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10
)`,
[
dispatch.id,
dispatch.approvalRequestId,
dispatch.projectId,
dispatch.action.actionType,
dispatch.action.actionRef,
dispatch.action.actionDigest,
dispatch.action.previewDigest,
JSON.stringify(dispatch),
approvedActionDispatchDigest(dispatch),
dispatch.createdAtMs,
],
);
if (result.rowCount !== 1) throw unavailable();
}
/** Administration-only PostgreSQL Approval/Approved Action authority. */
export class PostgresApprovalRequestRepository
implements ApprovalRequestRepository
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError('PostgreSQL Approval pool is invalid');
}
}
async #transaction<T>(
work: (client: PostgresClient) => Promise<T>,
): Promise<T> {
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
throw mappedError(error);
}
let began = false;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const result = await work(client);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
state &&
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
) {
continue;
}
throw mappedError(error);
} finally {
client.release();
}
}
throw unavailable();
}
async findById(
id: string,
): Promise<Readonly<ApprovalRequestRecord> | null> {
if (typeof id !== 'string' || !IDENTIFIER_PATTERN.test(id)) {
throw new TypeError('Approval request lookup identity is invalid');
}
try {
return await requestById(this.pool, id);
} catch (error) {
throw mappedError(error);
}
}
async findDispatchById(
id: string,
): Promise<Readonly<ApprovedActionDispatchRecord> | null> {
if (typeof id !== 'string' || !IDENTIFIER_PATTERN.test(id)) {
throw new TypeError('Approved action dispatch identity is invalid');
}
try {
return await dispatchById(this.pool, id);
} catch (error) {
throw mappedError(error);
}
}
create(
command: CreateApprovalRequestCommand,
): Promise<CreateApprovalRequestResult> {
const request = normalizeApprovalRequestRecord(command.request);
const audit = normalizeSecurityAuditRecord(command.audit);
if (
request.state !== 'pending' ||
request.version !== 1 ||
!auditMatches(audit, {
operationId: 'approval.request',
projectId: request.projectId,
subject: request.requestedBy,
outcome: 'approval_required',
fence: request.requestFence,
})
) {
throw new ApprovalMutationConflictError();
}
return this.#transaction(async (client) => {
const existing = await requestById(client, request.id);
if (existing) {
const storedAudit = await auditById(client, audit.eventId);
if (
!same(existing, request) ||
!storedAudit ||
!same(storedAudit, audit)
) {
throw new ApprovalMutationConflictError();
}
return Object.freeze({ status: 'existing' as const, request });
}
await assertFence(
client,
request.projectId,
request.requestedBy,
request.requestFence,
);
await insertRequest(client, request);
await insertAudit(client, audit);
return Object.freeze({ status: 'created' as const, request });
});
}
decide(
command: DecideDurableApprovalRequestCommand,
): Promise<DecideApprovalRequestResult> {
const audit = normalizeSecurityAuditRecord(command.audit);
return this.#transaction(async (client) => {
const current = await requestById(client, command.requestId);
if (!current) throw new ApprovalRequestNotFoundError();
const request = decideApprovalRequest(current, {
expectedVersion: command.expectedVersion,
decisionId: command.decisionId,
decision: command.decision,
reasonCode: command.reasonCode,
principal: command.principal,
decidedAtMs: command.decidedAtMs,
authorizationFence: command.authorizationFence,
});
if (
!auditMatches(audit, {
operationId: 'approval.decide',
projectId: request.projectId,
subject: command.principal.subject,
authenticationId: command.principal.authenticationId,
outcome: 'allowed',
fence: command.authorizationFence,
})
) {
throw new ApprovalMutationConflictError();
}
if (current.version === 2 && current.decisionId === command.decisionId) {
const storedAudit = await auditById(client, audit.eventId);
if (!storedAudit || !same(storedAudit, audit)) {
throw new ApprovalMutationConflictError();
}
return Object.freeze({ status: 'existing' as const, request });
}
await assertFence(
client,
request.projectId,
command.principal.subject,
command.authorizationFence,
);
await updateRequest(client, request, command.expectedVersion);
await insertAudit(client, audit);
return Object.freeze({ status: 'decided' as const, request });
});
}
consume(
command: ConsumeDurableApprovalRequestCommand,
): Promise<ConsumeDurableApprovalRequestResult> {
const audit = normalizeSecurityAuditRecord(command.audit);
return this.#transaction(async (client) => {
const current = await requestById(client, command.requestId);
if (!current) throw new ApprovalRequestNotFoundError();
const result = consumeApprovalRequest(current, {
expectedVersion: command.expectedVersion,
consumptionId: command.consumptionId,
dispatchId: command.dispatchId,
action: command.action,
requestedBy: command.requestedBy,
consumedBy: command.consumedBy,
consumedAtMs: command.consumedAtMs,
authorizationFence: command.authorizationFence,
});
if (
!auditMatches(audit, {
operationId: 'approval.consume',
projectId: result.request.projectId,
subject: command.consumedBy,
outcome: 'allowed',
fence: command.authorizationFence,
})
) {
throw new ApprovalMutationConflictError();
}
if (
current.version === 3 &&
current.consumptionId === command.consumptionId
) {
const storedAudit = await auditById(client, audit.eventId);
const dispatch = await dispatchById(client, result.dispatch.id);
const execution = await findPostgresApprovedActionExecution(
client,
result.dispatch.id,
);
if (
!storedAudit ||
!same(storedAudit, audit) ||
!dispatch ||
!same(dispatch, result.dispatch) ||
!execution ||
!same(execution, createApprovedActionExecution(result.dispatch))
) {
throw new ApprovalMutationConflictError();
}
return Object.freeze({
status: 'existing' as const,
request: result.request,
dispatch: result.dispatch,
});
}
await assertFence(
client,
result.request.projectId,
command.requestedBy,
command.authorizationFence,
);
await insertDispatch(client, result.dispatch);
await insertPostgresApprovedActionExecutionBaseline(
client,
result.dispatch,
);
await updateRequest(client, result.request, command.expectedVersion);
await insertAudit(client, audit);
return Object.freeze({
status: 'consumed' as const,
request: result.request,
dispatch: result.dispatch,
});
});
}
}
@@ -0,0 +1,229 @@
import type { PostgresPool } from '@qinglong/runtime-core';
import {
ApprovalUnavailableError,
approvalRequestDigest,
normalizeApprovalRequestRecord,
type ApprovalRequestRecord,
} from '@qinglong/runtime-core/approved-action';
import {
approvalRequestUpdatedAtMs,
assertApprovalDiscoveryProjectId,
assertApprovalDiscoveryRequestId,
assertApprovalRequestPageSize,
normalizeApprovalRequestCursor,
type ApprovalRequestDetail,
type ApprovalRequestDetailSource,
type ApprovalRequestPage,
type ApprovalRequestSource,
} from '@qinglong/runtime-core/approval-discovery';
import {
normalizeToolInvocationPreviewArtifact,
type ToolInvocationPreviewArtifact,
} from '@qinglong/runtime-core/tool-invocation-artifact';
import {
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
} from '../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
function unavailable(): ApprovalUnavailableError {
return new ApprovalUnavailableError();
}
function request(row: Row): Readonly<ApprovalRequestRecord> {
try {
const value = normalizeApprovalRequestRecord(
postgresRequiredJsonObject(
row.requestJson,
unavailable,
) as unknown as ApprovalRequestRecord,
);
if (
approvalRequestDigest(value) !==
postgresRequiredString(row.requestDigest, unavailable) ||
approvalRequestUpdatedAtMs(value) !==
postgresRequiredInteger(row.updatedAtMs, unavailable)
) {
throw unavailable();
}
return value;
} catch (error) {
if (error instanceof ApprovalUnavailableError) throw error;
throw unavailable();
}
}
function previewArtifact(
row: Row,
): Readonly<ToolInvocationPreviewArtifact> | null {
if (row.previewArtifactId === null) return null;
try {
const value = normalizeToolInvocationPreviewArtifact(
postgresRequiredJsonObject(
row.previewArtifactJson,
unavailable,
) as unknown as ToolInvocationPreviewArtifact,
);
if (
value.artifactId !== postgresRequiredString(row.previewArtifactId, unavailable) ||
value.projectId !== postgresRequiredString(row.previewProjectId, unavailable) ||
value.actionRef !== postgresRequiredString(row.previewActionRef, unavailable) ||
value.actionDigest !== postgresRequiredString(row.previewActionDigest, unavailable) ||
value.previewDigest !== postgresRequiredString(row.storedPreviewDigest, unavailable) ||
value.redactionContractDigest !==
postgresRequiredString(row.redactionContractDigest, unavailable) ||
value.artifactDigest !== postgresRequiredString(row.previewArtifactDigest, unavailable) ||
value.byteLength !== postgresRequiredInteger(row.previewByteLength, unavailable) ||
value.sealedAtMs !== postgresRequiredInteger(row.previewSealedAtMs, unavailable)
) {
throw unavailable();
}
return value;
} catch (error) {
if (error instanceof ApprovalUnavailableError) throw error;
throw unavailable();
}
}
/** Read-only Approval history using the existing Project keyset index. */
export class PostgresApprovalRequestSource
implements ApprovalRequestSource, ApprovalRequestDetailSource
{
constructor(private readonly pool: Pick<PostgresPool, 'query'>) {
if (!pool || typeof pool.query !== 'function') {
throw new TypeError('PostgreSQL Approval discovery pool is invalid');
}
}
async listApprovalRequests(options: {
readonly projectId: string;
readonly limit: number;
readonly after?: { readonly updatedAtMs: number; readonly requestId: string };
}): Promise<Readonly<ApprovalRequestPage>> {
if (!options || typeof options !== 'object' || Array.isArray(options)) {
throw new TypeError('Approval discovery options are invalid');
}
const keys = Object.keys(options);
if (
!keys.includes('projectId') ||
!keys.includes('limit') ||
keys.some((key) => !['projectId', 'limit', 'after'].includes(key))
) {
throw new TypeError('Approval discovery options shape is invalid');
}
assertApprovalDiscoveryProjectId(options.projectId);
assertApprovalRequestPageSize(options.limit);
const after = options.after
? normalizeApprovalRequestCursor(options.after)
: undefined;
try {
const result = await this.pool.query<Row>(
`SELECT request_json AS "requestJson",
request_digest AS "requestDigest",
updated_at_ms AS "updatedAtMs"
FROM "ql3"."approval_requests"
WHERE project_id = $1
AND (
$2::bigint IS NULL OR updated_at_ms < $2 OR
(updated_at_ms = $2 AND request_id < $3)
)
ORDER BY updated_at_ms DESC, request_id DESC
LIMIT $4`,
[
options.projectId,
after?.updatedAtMs ?? null,
after?.requestId ?? '',
options.limit + 1,
],
);
const truncated = result.rows.length > options.limit;
const requests = Object.freeze(
result.rows.slice(0, options.limit).map(request),
);
const last = requests.at(-1);
return Object.freeze({
requests,
truncated,
...(truncated && last
? {
next: Object.freeze({
updatedAtMs: approvalRequestUpdatedAtMs(last),
requestId: last.id,
}),
}
: {}),
});
} catch {
throw unavailable();
}
}
async getApprovalRequestDetail(options: {
readonly projectId: string;
readonly requestId: string;
}): Promise<Readonly<ApprovalRequestDetail> | null> {
if (
!options ||
typeof options !== 'object' ||
Array.isArray(options) ||
Object.keys(options).length !== 2 ||
!Object.hasOwn(options, 'projectId') ||
!Object.hasOwn(options, 'requestId')
) {
throw new TypeError('Approval detail options are invalid');
}
assertApprovalDiscoveryProjectId(options.projectId);
assertApprovalDiscoveryRequestId(options.requestId);
try {
const result = await this.pool.query<Row>(
`SELECT a.request_json AS "requestJson",
a.request_digest AS "requestDigest",
a.updated_at_ms AS "updatedAtMs",
p.artifact_id AS "previewArtifactId",
p.project_id AS "previewProjectId",
p.action_ref AS "previewActionRef",
p.action_digest AS "previewActionDigest",
p.preview_digest AS "storedPreviewDigest",
p.redaction_contract_digest AS "redactionContractDigest",
p.artifact_digest AS "previewArtifactDigest",
p.byte_length AS "previewByteLength",
p.sealed_at_ms AS "previewSealedAtMs",
p.artifact_json AS "previewArtifactJson"
FROM "ql3"."approval_requests" a
LEFT JOIN "ql3"."tool_invocation_preview_artifacts" p
ON a.action_type = 'tool.invoke'
AND p.project_id = a.project_id
AND p.action_ref = a.action_ref
AND p.action_digest = a.action_digest
AND p.preview_digest = a.preview_digest
WHERE a.project_id = $1 AND a.request_id = $2
LIMIT 2`,
[options.projectId, options.requestId],
);
if (result.rows.length > 1) throw unavailable();
if (!result.rows[0]) return null;
const approval = request(result.rows[0]);
const preview = previewArtifact(result.rows[0]);
if (
preview &&
(approval.action.actionType !== 'tool.invoke' ||
approval.projectId !== preview.projectId ||
approval.action.actionRef !== preview.actionRef ||
approval.action.actionDigest !== preview.actionDigest ||
approval.action.previewDigest !== preview.previewDigest)
) {
throw unavailable();
}
return Object.freeze({
request: approval,
preview: preview?.preview ?? null,
});
} catch (error) {
if (error instanceof ApprovalUnavailableError) throw error;
throw unavailable();
}
}
}
@@ -0,0 +1,512 @@
import type {
PostgresClient,
PostgresPool,
} from '@qinglong/runtime-core';
import {
normalizeApprovedActionDispatchRecord,
type ApprovedActionDispatchRecord,
} from '@qinglong/runtime-core/approved-action';
import {
ApprovedActionExecutionFenceConflictError,
ApprovedActionExecutionStateConflictError,
ApprovedActionExecutionUnavailableError,
approvedActionExecutionEffectiveStatus,
claimApprovedActionExecution,
completeApprovedActionExecution,
createApprovedActionExecution,
normalizeApprovedActionExecutionCursor,
normalizeApprovedActionExecutionRecord,
normalizeApprovedActionExecutionSnapshot,
releaseApprovedActionExecutionBeforeStart,
renewApprovedActionExecution,
startApprovedActionExecution,
type ApprovedActionExecutionRecord,
type ApprovedActionExecutionRepository,
type ApprovedActionExecutionSnapshot,
type ClaimApprovedActionExecutionCommand,
type ClaimApprovedActionExecutionResult,
type CompleteApprovedActionExecutionCommand,
type ListDueApprovedActionExecutionsQuery,
type ListDueApprovedActionExecutionsResult,
type ReleaseApprovedActionExecutionBeforeStartCommand,
type RenewApprovedActionExecutionCommand,
type StartApprovedActionExecutionCommand,
} from '@qinglong/runtime-core/approved-action-execution';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
type Queryable = Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>;
function unavailable(): ApprovedActionExecutionUnavailableError {
return new ApprovedActionExecutionUnavailableError();
}
function parseExecution(row: Row): Readonly<ApprovedActionExecutionRecord> {
try {
const execution = normalizeApprovedActionExecutionRecord(
postgresRequiredJsonObject(
row.executionJson,
unavailable,
) as unknown as ApprovedActionExecutionRecord,
);
if (
execution.executionDigest !==
postgresRequiredString(row.executionDigest, unavailable)
) {
throw unavailable();
}
return execution;
} catch (error) {
if (error instanceof ApprovedActionExecutionUnavailableError) throw error;
throw unavailable();
}
}
function parseDispatch(row: Row): Readonly<ApprovedActionDispatchRecord> {
try {
return normalizeApprovedActionDispatchRecord(
postgresRequiredJsonObject(
row.dispatchJson,
unavailable,
) as unknown as ApprovedActionDispatchRecord,
);
} catch {
throw unavailable();
}
}
function mappedError(error: unknown): Error {
if (
error instanceof ApprovedActionExecutionFenceConflictError ||
error instanceof ApprovedActionExecutionStateConflictError ||
error instanceof ApprovedActionExecutionUnavailableError ||
(error instanceof Error &&
error.name.startsWith('ApprovedActionExecution'))
) {
return error;
}
const state = postgresSqlState(error);
if (state === '23503' || state === '23505' || state === '23514') {
return new ApprovedActionExecutionStateConflictError();
}
return new ApprovedActionExecutionUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
function executionValues(
execution: Readonly<ApprovedActionExecutionRecord>,
): readonly (string | number | null)[] {
return [
execution.dispatchId,
execution.dispatchDigest,
execution.projectId,
execution.status,
execution.version,
execution.attemptCount,
execution.maxAttempts,
execution.eligibleAtMs,
execution.nextAttemptAtMs,
execution.leaseOwner,
execution.leaseToken,
execution.leaseExpiresAtMs,
execution.startedAtMs,
execution.resultMutationId,
execution.resultCode,
execution.resultDigest,
execution.completedAtMs,
execution.createdAtMs,
execution.updatedAtMs,
JSON.stringify(execution),
execution.executionDigest,
];
}
export async function insertPostgresApprovedActionExecutionBaseline(
client: PostgresClient,
dispatchValue: ApprovedActionDispatchRecord,
): Promise<Readonly<ApprovedActionExecutionRecord>> {
const execution = createApprovedActionExecution(dispatchValue);
const result = await client.query(
`INSERT INTO "ql3"."approved_action_executions" (
dispatch_id, dispatch_digest, project_id, status, version,
attempt_count, max_attempts, eligible_at_ms, next_attempt_at_ms,
lease_owner, lease_token, lease_expires_at_ms, started_at_ms,
result_mutation_id, result_code, result_digest, completed_at_ms,
created_at_ms, updated_at_ms, execution_json, execution_digest
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14,
$15, $16, $17, $18, $19, $20::jsonb, $21
)`,
executionValues(execution),
);
if (result.rowCount !== 1) throw unavailable();
return execution;
}
export async function findPostgresApprovedActionExecution(
queryable: Queryable,
dispatchId: string,
): Promise<Readonly<ApprovedActionExecutionRecord> | null> {
const result = await queryable.query<Row>(
`SELECT execution_json AS "executionJson",
execution_digest AS "executionDigest"
FROM "ql3"."approved_action_executions"
WHERE dispatch_id = $1
LIMIT 2`,
[dispatchId],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return parseExecution(result.rows[0]!);
}
async function loadSnapshot(
queryable: Queryable,
dispatchId: string,
lock = false,
): Promise<Readonly<ApprovedActionExecutionSnapshot> | null> {
const result = await queryable.query<Row>(
`SELECT execution.execution_json AS "executionJson",
execution.execution_digest AS "executionDigest",
dispatch.dispatch_json AS "dispatchJson"
FROM "ql3"."approved_action_executions" AS execution
JOIN "ql3"."approved_action_dispatches" AS dispatch
ON dispatch.dispatch_id = execution.dispatch_id
WHERE execution.dispatch_id = $1
LIMIT 2
${lock ? 'FOR UPDATE OF execution' : ''}`,
[dispatchId],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return normalizeApprovedActionExecutionSnapshot({
dispatch: parseDispatch(result.rows[0]!),
execution: parseExecution(result.rows[0]!),
});
}
async function replaceExecution(
client: PostgresClient,
previous: Readonly<ApprovedActionExecutionRecord>,
next: Readonly<ApprovedActionExecutionRecord>,
): Promise<void> {
const values = executionValues(next);
const result = await client.query(
`UPDATE "ql3"."approved_action_executions"
SET dispatch_digest = $1, project_id = $2, status = $3,
version = $4, attempt_count = $5, max_attempts = $6,
eligible_at_ms = $7, next_attempt_at_ms = $8, lease_owner = $9,
lease_token = $10, lease_expires_at_ms = $11, started_at_ms = $12,
result_mutation_id = $13, result_code = $14, result_digest = $15,
completed_at_ms = $16, created_at_ms = $17, updated_at_ms = $18,
execution_json = $19::jsonb, execution_digest = $20
WHERE dispatch_id = $21 AND version = $22
AND execution_digest = $23`,
[
...values.slice(1),
previous.dispatchId,
previous.version,
previous.executionDigest,
],
);
if (result.rowCount !== 1) {
throw new ApprovedActionExecutionFenceConflictError();
}
}
function assertPageSize(value: number): void {
if (!Number.isSafeInteger(value) || value < 1 || value > 64) {
throw new ApprovedActionExecutionStateConflictError();
}
}
function actionTypes(values: readonly string[]): readonly string[] {
if (
!Array.isArray(values) ||
values.length > 64 ||
values.some(
(value) =>
typeof value !== 'string' ||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value),
) ||
new Set(values).size !== values.length
) {
throw new ApprovedActionExecutionStateConflictError();
}
return values;
}
export class PostgresApprovedActionExecutionRepository
implements ApprovedActionExecutionRepository
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError('PostgreSQL Approved Action execution pool is invalid');
}
}
async #transaction<T>(
work: (client: PostgresClient) => Promise<T>,
): Promise<T> {
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
throw mappedError(error);
}
let began = false;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const result = await work(client);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
state &&
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
) {
continue;
}
throw mappedError(error);
} finally {
client.release();
}
}
throw unavailable();
}
async findExecutionByDispatchId(
dispatchId: string,
): Promise<Readonly<ApprovedActionExecutionSnapshot> | null> {
try {
return await loadSnapshot(this.pool, dispatchId);
} catch (error) {
throw mappedError(error);
}
}
async listDueExecutions(
query: ListDueApprovedActionExecutionsQuery,
): Promise<ListDueApprovedActionExecutionsResult> {
assertPageSize(query.limit);
const handledActionTypes = actionTypes(query.actionTypes);
if (!Number.isSafeInteger(query.nowMs) || query.nowMs < 0) {
throw new ApprovedActionExecutionStateConflictError();
}
if (handledActionTypes.length === 0) {
return Object.freeze({ executions: Object.freeze([]), truncated: false });
}
const cursor = query.cursor
? normalizeApprovedActionExecutionCursor(query.cursor)
: undefined;
try {
const result = await this.pool.query<Row>(
`SELECT execution.execution_json AS "executionJson",
execution.execution_digest AS "executionDigest",
dispatch.dispatch_json AS "dispatchJson"
FROM "ql3"."approved_action_executions" AS execution
JOIN "ql3"."approved_action_dispatches" AS dispatch
ON dispatch.dispatch_id = execution.dispatch_id
WHERE execution.status IN ('pending','leased','retry_wait')
AND execution.eligible_at_ms <= $1
AND dispatch.action_type = ANY($2::varchar[])
AND (
$3::varchar IS NULL OR execution.eligible_at_ms > $4 OR
(
execution.eligible_at_ms = $4
AND execution.dispatch_id > $3
)
)
ORDER BY execution.eligible_at_ms, execution.dispatch_id
LIMIT $5`,
[
query.nowMs,
handledActionTypes,
cursor?.dispatchId ?? null,
cursor?.eligibleAtMs ?? 0,
query.limit + 1,
],
);
const truncated = result.rows.length > query.limit;
const rows = truncated
? result.rows.slice(0, query.limit)
: result.rows;
const executions = rows.map((row) =>
normalizeApprovedActionExecutionSnapshot({
dispatch: parseDispatch(row),
execution: parseExecution(row),
}),
);
const last = executions.at(-1)?.execution;
return Object.freeze({
executions: Object.freeze(executions),
truncated,
...(truncated && last?.eligibleAtMs !== null
? {
nextCursor: Object.freeze({
eligibleAtMs: last!.eligibleAtMs!,
dispatchId: last!.dispatchId,
}),
}
: {}),
});
} catch (error) {
throw mappedError(error);
}
}
claimExecution(
command: ClaimApprovedActionExecutionCommand,
): Promise<ClaimApprovedActionExecutionResult> {
return this.#transaction(async (client) => {
const current = await loadSnapshot(client, command.dispatchId, true);
if (!current) return Object.freeze({ status: 'not_found' as const });
const effective = approvedActionExecutionEffectiveStatus(
current.execution,
command.nowMs,
);
if (
current.execution.status === 'leased' &&
current.execution.leaseExpiresAtMs !== null &&
current.execution.leaseExpiresAtMs <= command.nowMs &&
current.execution.attemptCount >= current.execution.maxAttempts
) {
return Object.freeze({
status: 'recovery_required' as const,
snapshot: current,
});
}
const due =
current.execution.eligibleAtMs !== null &&
current.execution.eligibleAtMs <= command.nowMs;
if (
effective !== 'pending' &&
effective !== 'retry_wait' &&
!(effective === 'leased' && due)
) {
return Object.freeze({ status: effective, snapshot: current });
}
if (!due) {
return Object.freeze({
status: 'not_due' as const,
snapshot: current,
});
}
const next = claimApprovedActionExecution(current.execution, {
owner: command.owner,
leaseToken: command.leaseToken,
nowMs: command.nowMs,
leaseDurationMs: command.leaseDurationMs,
});
await replaceExecution(client, current.execution, next);
return Object.freeze({
status: 'claimed' as const,
snapshot: Object.freeze({
dispatch: current.dispatch,
execution: next,
}),
});
});
}
startExecution(
command: StartApprovedActionExecutionCommand,
): Promise<Readonly<ApprovedActionExecutionSnapshot>> {
return this.#mutate(command.dispatchId, (current) =>
startApprovedActionExecution(current, command),
);
}
renewExecution(
command: RenewApprovedActionExecutionCommand,
): Promise<Readonly<ApprovedActionExecutionSnapshot>> {
return this.#mutate(command.dispatchId, (current) =>
renewApprovedActionExecution(current.execution, {
owner: command.owner,
leaseToken: command.leaseToken,
expectedVersion: command.expectedVersion,
nowMs: command.nowMs,
leaseDurationMs: command.leaseDurationMs,
}),
);
}
releaseExecutionBeforeStart(
command: ReleaseApprovedActionExecutionBeforeStartCommand,
): Promise<Readonly<ApprovedActionExecutionSnapshot>> {
return this.#mutate(command.dispatchId, (current) =>
releaseApprovedActionExecutionBeforeStart(current.execution, {
owner: command.owner,
leaseToken: command.leaseToken,
expectedVersion: command.expectedVersion,
resultMutationId: command.resultMutationId,
resultCode: command.resultCode,
atMs: command.atMs,
...(command.retryAtMs === undefined
? {}
: { retryAtMs: command.retryAtMs }),
}),
);
}
completeExecution(
command: CompleteApprovedActionExecutionCommand,
): Promise<Readonly<ApprovedActionExecutionSnapshot>> {
return this.#mutate(command.dispatchId, (current) =>
completeApprovedActionExecution(current.execution, {
owner: command.owner,
leaseToken: command.leaseToken,
expectedVersion: command.expectedVersion,
resultMutationId: command.resultMutationId,
outcome: command.outcome,
resultCode: command.resultCode,
...(command.resultDigest === undefined
? {}
: { resultDigest: command.resultDigest }),
completedAtMs: command.completedAtMs,
}),
);
}
#mutate(
dispatchId: string,
transition: (
current: Readonly<ApprovedActionExecutionSnapshot>,
) => Readonly<ApprovedActionExecutionRecord>,
): Promise<Readonly<ApprovedActionExecutionSnapshot>> {
return this.#transaction(async (client) => {
const current = await loadSnapshot(client, dispatchId, true);
if (!current) throw new ApprovedActionExecutionStateConflictError();
const next = transition(current);
await replaceExecution(client, current.execution, next);
return Object.freeze({
dispatch: current.dispatch,
execution: next,
});
});
}
}
@@ -0,0 +1,440 @@
// PostgreSQL authorized administration authority for Task and Trigger definitions.
import type {
PostgresClient,
PostgresPool,
} from '@qinglong/runtime-core';
import {
InvalidTaskDefinitionAdministrationReadError,
TaskDefinitionAdministrationAuthorizationFenceConflictError,
TaskDefinitionAdministrationMutationConflictError,
TaskDefinitionAdministrationReadConflictError,
normalizeAuthorizedTaskDefinitionInspection,
normalizeAuthorizedTaskDefinitionList,
normalizeAuthorizedTaskDefinitionRevisionMutation,
type AuthorizedTaskDefinitionInspection,
type AuthorizedTaskDefinitionList,
type AuthorizedTaskDefinitionRevisionMutation,
type TaskDefinitionAdministrationRepository,
type TaskDefinitionAdministrationSource,
} from '@qinglong/runtime-core/task-definition-administration';
import {
InvalidTriggerAdministrationReadError,
TriggerAdministrationAuthorizationFenceConflictError,
TriggerAdministrationMutationConflictError,
TriggerAdministrationReadConflictError,
normalizeAuthorizedTriggerInspection,
normalizeAuthorizedTriggerList,
normalizeAuthorizedTriggerRevisionMutation,
type AuthorizedTriggerInspection,
type AuthorizedTriggerList,
type AuthorizedTriggerRevisionMutation,
type TriggerAdministrationRepository,
type TriggerAdministrationSource,
} from '@qinglong/runtime-core/trigger-administration';
import {
TaskDefinitionUnavailableError,
type TaskDefinitionPage,
type TaskDefinitionRecord,
} from '@qinglong/runtime-core/task-definition';
import {
TriggerUnavailableError,
type TriggerPage,
type TriggerRecord,
} from '@qinglong/runtime-core/trigger';
import {
ADMINISTRATION_AUDIT_SELECT,
auditFromRow,
configureAdministrationTransaction,
insertAdministrationAudit,
requiredInteger,
requiredString,
retryableAdministrationError,
rollbackAdministrationTransaction,
sameAdministrationReplayAudit,
type AdministrationAuditRow,
type AdministrationRow,
} from '../repository/administrationSupport';
import {
PostgresTaskDefinitionRepository,
PostgresTaskDefinitionSource,
} from './taskDefinitionRepository';
import {
PostgresTriggerRepository,
PostgresTriggerSource,
} from '../scheduling/triggerRepository';
type FenceConflict = new () => Error;
type MutationConflict = new () => Error;
type ReadConflict = new () => Error;
type StorageUnavailable = new () => Error;
type InvalidRead = new (message: string) => Error;
const AUTHORIZED_READ_TRANSACTION_ATTEMPTS = 3;
function assertPool(pool: PostgresPool): void {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError('PostgreSQL automation administration pool is invalid');
}
}
async function confirmAuthorizationFence(
client: PostgresClient,
mutation: Readonly<{
command: Readonly<{ projectId: string }>;
actor: Readonly<{ type: string; id: string }>;
fence: Readonly<{ projectVersion: number; bindingVersion: number | null }>;
}>,
FenceConflictError: FenceConflict,
): Promise<void> {
try {
const project = await client.query<AdministrationRow>(
`SELECT status, version
FROM "ql3"."projects"
WHERE id = $1`,
[mutation.command.projectId],
);
const binding = await client.query<AdministrationRow>(
`SELECT version, state
FROM "ql3"."project_role_bindings"
WHERE project_id = $1 AND subject_type = $2 AND subject_id = $3
ORDER BY version DESC
LIMIT 1`,
[
mutation.command.projectId,
mutation.actor.type,
mutation.actor.id,
],
);
if (
project.rows.length !== 1 ||
requiredString(project.rows[0]!, 'status') !== 'active' ||
requiredInteger(project.rows[0]!, 'version') !==
mutation.fence.projectVersion ||
binding.rows.length !== 1 ||
requiredString(binding.rows[0]!, 'state') !== 'active' ||
requiredInteger(binding.rows[0]!, 'version') !==
mutation.fence.bindingVersion
) {
throw new FenceConflictError();
}
} catch (error) {
if (error instanceof FenceConflictError) throw error;
throw new FenceConflictError();
}
}
async function confirmOrAppendAllowedAudit(
client: PostgresClient,
replay: boolean,
audit: AuthorizedTaskDefinitionRevisionMutation['audit'],
MutationConflictError: MutationConflict,
): Promise<void> {
const result = await client.query<AdministrationAuditRow>(
`SELECT ${ADMINISTRATION_AUDIT_SELECT}
FROM "ql3"."security_audit_events" AS audit
WHERE audit.event_id = $1
LIMIT 2`,
[audit.eventId],
);
if (replay) {
if (result.rows.length !== 1) throw new MutationConflictError();
try {
if (!sameAdministrationReplayAudit(auditFromRow(result.rows[0]!), audit)) {
throw new MutationConflictError();
}
} catch (error) {
if (error instanceof MutationConflictError) throw error;
throw new MutationConflictError();
}
return;
}
if (result.rows.length !== 0) throw new MutationConflictError();
await insertAdministrationAudit(client, audit);
}
function clientReadPool(client: PostgresClient): PostgresPool {
return Object.freeze({
query: client.query.bind(client),
async connect() {
return client;
},
});
}
function sqlState(error: unknown): string | undefined {
if (!error || typeof error !== 'object') return undefined;
const code = (error as { code?: unknown }).code;
return typeof code === 'string' ? code : undefined;
}
async function confirmReadAuthorizationFence(
client: PostgresClient,
read: Readonly<{
projectId: string;
actor: Readonly<{ type: string; id: string }>;
fence: Readonly<{ projectVersion: number; bindingVersion: number | null }>;
}>,
FenceConflictError: FenceConflict,
): Promise<void> {
try {
const project = await client.query<AdministrationRow>(
`SELECT status, version
FROM "ql3"."projects"
WHERE id = $1
LIMIT 1`,
[read.projectId],
);
const binding = await client.query<AdministrationRow>(
`SELECT version, state
FROM "ql3"."project_role_bindings"
WHERE project_id = $1 AND subject_type = $2 AND subject_id = $3
ORDER BY version DESC
LIMIT 1`,
[read.projectId, read.actor.type, read.actor.id],
);
if (
project.rows.length !== 1 ||
requiredString(project.rows[0]!, 'status') !== 'active' ||
requiredInteger(project.rows[0]!, 'version') !==
read.fence.projectVersion ||
binding.rows.length !== 1 ||
requiredString(binding.rows[0]!, 'state') !== 'active' ||
requiredInteger(binding.rows[0]!, 'version') !==
read.fence.bindingVersion
) {
throw new FenceConflictError();
}
} catch (error) {
if (error instanceof FenceConflictError) throw error;
throw new FenceConflictError();
}
}
async function assertFreshReadAudit(
client: PostgresClient,
eventId: string,
ReadConflictError: ReadConflict,
): Promise<void> {
const result = await client.query<AdministrationRow>(
`SELECT event_id
FROM "ql3"."security_audit_events"
WHERE event_id = $1
LIMIT 2`,
[eventId],
);
if (result.rows.length !== 0) throw new ReadConflictError();
}
async function executeAuthorizedRead<TResult>(
pool: PostgresPool,
read: Readonly<{
projectId: string;
actor: Readonly<{ type: string; id: string }>;
fence: Readonly<{ projectVersion: number; bindingVersion: number | null }>;
audit: AuthorizedTaskDefinitionInspection['audit'];
}>,
FenceConflictError: FenceConflict,
ReadConflictError: ReadConflict,
InvalidReadError: InvalidRead,
UnavailableError: StorageUnavailable,
operation: (client: PostgresClient) => Promise<TResult>,
): Promise<TResult> {
for (
let attempt = 0;
attempt < AUTHORIZED_READ_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await pool.connect();
} catch {
throw new UnavailableError();
}
let began = false;
try {
await configureAdministrationTransaction(client);
began = true;
await confirmReadAuthorizationFence(client, read, FenceConflictError);
await assertFreshReadAudit(
client,
read.audit.eventId,
ReadConflictError,
);
const result = await operation(client);
await insertAdministrationAudit(client, read.audit);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (began) await rollbackAdministrationTransaction(client);
if (
error instanceof FenceConflictError ||
error instanceof ReadConflictError ||
error instanceof InvalidReadError ||
error instanceof UnavailableError
) {
throw error;
}
if (sqlState(error) === '23505') throw new ReadConflictError();
if (
retryableAdministrationError(error) &&
attempt + 1 < AUTHORIZED_READ_TRANSACTION_ATTEMPTS
) {
continue;
}
throw new UnavailableError();
} finally {
client.release();
}
}
throw new UnavailableError();
}
/** Least-privilege automation-manager Task mutation adapter. */
export class PostgresTaskDefinitionAdministrationRepository
implements
TaskDefinitionAdministrationRepository,
TaskDefinitionAdministrationSource
{
private readonly taskDefinitions: PostgresTaskDefinitionRepository;
constructor(private readonly pool: PostgresPool) {
assertPool(pool);
this.taskDefinitions = new PostgresTaskDefinitionRepository(pool);
}
appendAuthorizedTaskDefinitionRevision(
input: AuthorizedTaskDefinitionRevisionMutation,
) {
const mutation = normalizeAuthorizedTaskDefinitionRevisionMutation(input);
return this.taskDefinitions.appendTaskDefinitionRevision(
mutation.command,
async (client, { replay }) => {
await confirmAuthorizationFence(
client,
mutation,
TaskDefinitionAdministrationAuthorizationFenceConflictError,
);
await confirmOrAppendAllowedAudit(
client,
replay !== null,
mutation.audit,
TaskDefinitionAdministrationMutationConflictError,
);
},
);
}
findAuthorizedCurrentTaskDefinition(
input: AuthorizedTaskDefinitionInspection,
): Promise<TaskDefinitionRecord | null> {
const inspection = normalizeAuthorizedTaskDefinitionInspection(input);
return executeAuthorizedRead(
this.pool,
inspection,
TaskDefinitionAdministrationAuthorizationFenceConflictError,
TaskDefinitionAdministrationReadConflictError,
InvalidTaskDefinitionAdministrationReadError,
TaskDefinitionUnavailableError,
(client) =>
new PostgresTaskDefinitionSource(
clientReadPool(client),
).findCurrentTaskDefinition(inspection.projectId, inspection.taskId),
);
}
listAuthorizedTaskDefinitions(
input: AuthorizedTaskDefinitionList,
): Promise<TaskDefinitionPage> {
const query = normalizeAuthorizedTaskDefinitionList(input);
return executeAuthorizedRead(
this.pool,
query,
TaskDefinitionAdministrationAuthorizationFenceConflictError,
TaskDefinitionAdministrationReadConflictError,
InvalidTaskDefinitionAdministrationReadError,
TaskDefinitionUnavailableError,
(client) =>
new PostgresTaskDefinitionSource(clientReadPool(client)).listTaskDefinitions(
{
projectId: query.projectId,
limit: query.limit,
...(query.after ? { after: query.after } : {}),
},
),
);
}
}
/** Least-privilege automation-manager Trigger mutation adapter. */
export class PostgresTriggerAdministrationRepository
implements TriggerAdministrationRepository, TriggerAdministrationSource
{
private readonly triggers: PostgresTriggerRepository;
constructor(private readonly pool: PostgresPool) {
assertPool(pool);
this.triggers = new PostgresTriggerRepository(pool);
}
appendAuthorizedTriggerRevision(input: AuthorizedTriggerRevisionMutation) {
const mutation = normalizeAuthorizedTriggerRevisionMutation(input);
return this.triggers.appendTriggerRevision(
mutation.command,
async (client, { replay }) => {
await confirmAuthorizationFence(
client,
mutation,
TriggerAdministrationAuthorizationFenceConflictError,
);
await confirmOrAppendAllowedAudit(
client,
replay !== null,
mutation.audit,
TriggerAdministrationMutationConflictError,
);
},
);
}
findAuthorizedCurrentTrigger(
input: AuthorizedTriggerInspection,
): Promise<TriggerRecord | null> {
const inspection = normalizeAuthorizedTriggerInspection(input);
return executeAuthorizedRead(
this.pool,
inspection,
TriggerAdministrationAuthorizationFenceConflictError,
TriggerAdministrationReadConflictError,
InvalidTriggerAdministrationReadError,
TriggerUnavailableError,
(client) =>
new PostgresTriggerSource(clientReadPool(client)).findCurrentTrigger(
inspection.projectId,
inspection.triggerId,
),
);
}
listAuthorizedTriggers(input: AuthorizedTriggerList): Promise<TriggerPage> {
const query = normalizeAuthorizedTriggerList(input);
return executeAuthorizedRead(
this.pool,
query,
TriggerAdministrationAuthorizationFenceConflictError,
TriggerAdministrationReadConflictError,
InvalidTriggerAdministrationReadError,
TriggerUnavailableError,
(client) =>
new PostgresTriggerSource(clientReadPool(client)).listTriggers({
projectId: query.projectId,
limit: query.limit,
...(query.after ? { after: query.after } : {}),
}),
);
}
}
@@ -0,0 +1,710 @@
// PostgreSQL authority for Task Definition publication and execution revisions.
import {
InvalidTaskDefinitionError,
TaskDefinitionConflictError,
TaskDefinitionUnavailableError,
assertTaskDefinitionIdentifier,
assertTaskDefinitionPageSize,
assertTaskDefinitionRevision,
createTaskDefinitionRecord,
normalizeAppendTaskDefinitionRevisionCommand,
normalizeTaskDefinitionCursor,
normalizeTaskDefinitionRecord,
type AppendTaskDefinitionRevisionCommand,
type TaskDefinitionPage,
type TaskDefinitionRecord,
type TaskDefinitionRepository,
type TaskDefinitionSource,
} from '@qinglong/runtime-core/task-definition';
import {
BUILT_IN_COMMAND_TASK_SPEC_SCHEMA,
TaskSpecSemanticRegistry,
createBuiltInTaskSpecSemanticRegistry,
} from '@qinglong/runtime-core/task-spec-semantic';
import {
compileClusterCommandTaskDefinition,
normalizeClusterTaskExecutionRevision,
type ClusterTaskExecutionRevision,
type ClusterTaskExecutionRevisionSource,
} from '@qinglong/runtime-core/cluster-execution-revision';
import type {
PostgresClient,
PostgresPool,
PostgresQueryResult,
} from '@qinglong/runtime-core';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredBoolean,
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../repository/definitionRepositorySupport';
type TaskDefinitionRow = Record<string, unknown>;
type Queryable = Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>;
export interface PostgresTaskDefinitionRevisionTransactionContext {
readonly command: Readonly<AppendTaskDefinitionRevisionCommand>;
readonly replay: Readonly<TaskDefinitionRecord> | null;
readonly record: Readonly<TaskDefinitionRecord>;
}
export type PostgresTaskDefinitionRevisionTransactionHook = (
client: PostgresClient,
context: Readonly<PostgresTaskDefinitionRevisionTransactionContext>,
) => Promise<void>;
const SELECT_FIELDS = `
head.project_id AS "projectId",
head.task_id AS "taskId",
revision.revision,
revision.mutation_id AS "mutationId",
revision.name,
revision.description,
revision.kind,
revision.spec_json AS "specJson",
revision.labels_json AS "labelsJson",
revision.enabled,
revision.content_digest AS "contentDigest",
head.created_at_ms AS "createdAtMs",
revision.created_at_ms AS "updatedAtMs"`;
function unavailable(): TaskDefinitionUnavailableError {
return new TaskDefinitionUnavailableError();
}
function taskDefinitionRecord(
row: TaskDefinitionRow,
): TaskDefinitionRecord {
try {
const description = row.description;
if (description !== null && typeof description !== 'string') {
throw unavailable();
}
return normalizeTaskDefinitionRecord({
projectId: postgresRequiredString(row.projectId, unavailable),
taskId: postgresRequiredString(row.taskId, unavailable),
revision: postgresRequiredInteger(row.revision, unavailable),
mutationId: postgresRequiredString(row.mutationId, unavailable),
name: postgresRequiredString(row.name, unavailable),
...(description === null ? {} : { description }),
kind: postgresRequiredString(
row.kind,
unavailable,
) as TaskDefinitionRecord['kind'],
spec: postgresRequiredJsonObject(
row.specJson,
unavailable,
) as unknown as TaskDefinitionRecord['spec'],
labels: postgresRequiredJsonObject(
row.labelsJson,
unavailable,
) as TaskDefinitionRecord['labels'],
enabled: postgresRequiredBoolean(row.enabled, unavailable),
contentDigest: postgresRequiredString(row.contentDigest, unavailable),
createdAtMs: postgresRequiredInteger(row.createdAtMs, unavailable),
updatedAtMs: postgresRequiredInteger(row.updatedAtMs, unavailable),
});
} catch (error) {
if (error instanceof TaskDefinitionUnavailableError) throw error;
throw unavailable();
}
}
function sameRecord(
left: TaskDefinitionRecord,
right: TaskDefinitionRecord,
): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function executionPlanJson(
revision: ClusterTaskExecutionRevision,
): Readonly<Record<string, unknown>> {
return Object.freeze({
command: revision.command,
environment: revision.environment,
...(revision.workingDirectory === undefined
? {}
: { workingDirectory: revision.workingDirectory }),
...(revision.timeoutMs === undefined
? {}
: { timeoutMs: revision.timeoutMs }),
...(revision.placement === undefined
? {}
: { placement: revision.placement }),
});
}
function executionRevision(row: TaskDefinitionRow): ClusterTaskExecutionRevision {
try {
const plan = postgresRequiredJsonObject(row.planJson, unavailable);
const keys = Object.keys(plan);
if (
!keys.includes('command') ||
!keys.includes('environment') ||
keys.some(
(key) =>
!['command', 'environment', 'placement', 'timeoutMs', 'workingDirectory'].includes(
key,
),
)
) {
throw unavailable();
}
return normalizeClusterTaskExecutionRevision({
projectId: postgresRequiredString(row.projectId, unavailable),
taskId: postgresRequiredString(row.taskId, unavailable),
sourceRevision: postgresRequiredInteger(row.sourceRevision, unavailable),
taskRevision: postgresRequiredString(row.taskRevision, unavailable),
sourceContentDigest: postgresRequiredString(
row.sourceContentDigest,
unavailable,
),
executorType: postgresRequiredString(
row.executorType,
unavailable,
) as ClusterTaskExecutionRevision['executorType'],
planSchema: postgresRequiredString(
row.planSchema,
unavailable,
) as ClusterTaskExecutionRevision['planSchema'],
command: plan.command as ClusterTaskExecutionRevision['command'],
environment:
plan.environment as ClusterTaskExecutionRevision['environment'],
...(plan.workingDirectory === undefined
? {}
: { workingDirectory: plan.workingDirectory as string }),
...(plan.timeoutMs === undefined
? {}
: { timeoutMs: plan.timeoutMs as number }),
...(plan.placement === undefined
? {}
: {
placement:
plan.placement as unknown as NonNullable<
ClusterTaskExecutionRevision['placement']
>,
}),
contentDigest: postgresRequiredString(row.contentDigest, unavailable),
createdAtMs: postgresRequiredInteger(row.createdAtMs, unavailable),
});
} catch (error) {
if (error instanceof TaskDefinitionUnavailableError) throw error;
throw unavailable();
}
}
export async function findExecutionRevision(
queryable: Queryable,
projectId: string,
taskId: string,
sourceRevision: number,
): Promise<ClusterTaskExecutionRevision | null> {
const result = await queryable.query<TaskDefinitionRow>(
`SELECT project_id AS "projectId", task_id AS "taskId",
source_revision AS "sourceRevision",
task_revision AS "taskRevision",
source_content_digest AS "sourceContentDigest",
executor_type AS "executorType", plan_schema AS "planSchema",
plan_json AS "planJson", content_digest AS "contentDigest",
created_at_ms AS "createdAtMs"
FROM "ql3"."task_execution_revisions"
WHERE project_id = $1 AND task_id = $2 AND source_revision = $3
AND executor_type = 'remote_worker'
LIMIT 2`,
[projectId, taskId, sourceRevision],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return executionRevision(result.rows[0]!);
}
function sameExecutionRevision(
left: ClusterTaskExecutionRevision | null,
right: ClusterTaskExecutionRevision | null,
): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function mappedError(error: unknown): Error {
if (
error instanceof TaskDefinitionConflictError ||
error instanceof TaskDefinitionUnavailableError
) {
return error;
}
const state = postgresSqlState(error);
if (state === '23503' || state === '23505' || state === '23514') {
return new TaskDefinitionConflictError();
}
return unavailable();
}
async function findCurrent(
queryable: Queryable,
projectId: string,
taskId: string,
): Promise<TaskDefinitionRecord | null> {
const result = await queryable.query<TaskDefinitionRow>(
`SELECT ${SELECT_FIELDS}
FROM "ql3"."task_definitions" AS head
LEFT JOIN "ql3"."task_definition_revisions" AS revision
ON revision.project_id = head.project_id
AND revision.task_id = head.task_id
AND revision.revision = head.current_revision
WHERE head.project_id = $1 AND head.task_id = $2
LIMIT 2`,
[projectId, taskId],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return taskDefinitionRecord(result.rows[0]!);
}
async function findRevision(
queryable: Queryable,
projectId: string,
taskId: string,
revision: number,
): Promise<TaskDefinitionRecord | null> {
const result = await queryable.query<TaskDefinitionRow>(
`SELECT ${SELECT_FIELDS}
FROM "ql3"."task_definitions" AS head
JOIN "ql3"."task_definition_revisions" AS revision
ON revision.project_id = head.project_id
AND revision.task_id = head.task_id
WHERE head.project_id = $1 AND head.task_id = $2
AND revision.revision = $3
LIMIT 2`,
[projectId, taskId, revision],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return taskDefinitionRecord(result.rows[0]!);
}
async function findByMutation(
queryable: Queryable,
mutationId: string,
): Promise<TaskDefinitionRecord | null> {
const result = await queryable.query<TaskDefinitionRow>(
`SELECT ${SELECT_FIELDS}
FROM "ql3"."task_definition_revisions" AS revision
JOIN "ql3"."task_definitions" AS head
ON head.project_id = revision.project_id
AND head.task_id = revision.task_id
WHERE revision.mutation_id = $1
LIMIT 2`,
[mutationId],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return taskDefinitionRecord(result.rows[0]!);
}
/** Read-only PostgreSQL TaskDefinition port for the runtime role. */
export class PostgresTaskDefinitionSource implements TaskDefinitionSource {
constructor(protected readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError('PostgreSQL TaskDefinition pool is invalid');
}
}
async findCurrentTaskDefinition(
projectId: string,
taskId: string,
): Promise<TaskDefinitionRecord | null> {
assertTaskDefinitionIdentifier(projectId, 'projectId');
assertTaskDefinitionIdentifier(taskId, 'taskId');
try {
return await findCurrent(this.pool, projectId, taskId);
} catch (error) {
throw mappedError(error);
}
}
async findTaskDefinitionRevision(
projectId: string,
taskId: string,
revision: number,
): Promise<TaskDefinitionRecord | null> {
assertTaskDefinitionIdentifier(projectId, 'projectId');
assertTaskDefinitionIdentifier(taskId, 'taskId');
assertTaskDefinitionRevision(revision);
try {
return await findRevision(this.pool, projectId, taskId, revision);
} catch (error) {
throw mappedError(error);
}
}
async listTaskDefinitions(options: {
readonly projectId: string;
readonly limit: number;
readonly after?: { readonly taskId: string };
}): Promise<TaskDefinitionPage> {
if (!options || typeof options !== 'object' || Array.isArray(options)) {
throw new InvalidTaskDefinitionError('list options are invalid');
}
const keys = Object.keys(options);
if (
!keys.includes('limit') ||
!keys.includes('projectId') ||
keys.some((key) => !['after', 'limit', 'projectId'].includes(key))
) {
throw new InvalidTaskDefinitionError(
'list options have an invalid shape',
);
}
assertTaskDefinitionIdentifier(options.projectId, 'projectId');
assertTaskDefinitionPageSize(options.limit);
const after = options.after
? normalizeTaskDefinitionCursor(options.after)
: undefined;
try {
const result = await this.pool.query<TaskDefinitionRow>(
`SELECT ${SELECT_FIELDS}
FROM "ql3"."task_definitions" AS head
LEFT JOIN "ql3"."task_definition_revisions" AS revision
ON revision.project_id = head.project_id
AND revision.task_id = head.task_id
AND revision.revision = head.current_revision
WHERE head.project_id = $1 AND head.task_id > $2
ORDER BY head.task_id
LIMIT $3`,
[options.projectId, after?.taskId ?? '', options.limit + 1],
);
const truncated = result.rows.length > options.limit;
const definitions = Object.freeze(
result.rows.slice(0, options.limit).map(taskDefinitionRecord),
);
const last = definitions.at(-1);
return Object.freeze({
definitions,
truncated,
...(truncated && last
? { next: Object.freeze({ taskId: last.taskId }) }
: {}),
});
} catch (error) {
throw mappedError(error);
}
}
}
/** Read-only, digest-verifying remote Worker execution revision source. */
export class PostgresTaskExecutionRevisionSource
implements ClusterTaskExecutionRevisionSource
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError('PostgreSQL Task execution revision pool is invalid');
}
}
async resolveClusterTaskExecutionRevision(identity: {
readonly projectId: string;
readonly taskId: string;
readonly sourceRevision: number;
}): Promise<ClusterTaskExecutionRevision | null> {
if (!identity || typeof identity !== 'object' || Array.isArray(identity)) {
throw new TypeError('Cluster Task execution revision identity is invalid');
}
assertTaskDefinitionIdentifier(identity.projectId, 'projectId');
assertTaskDefinitionIdentifier(identity.taskId, 'taskId');
assertTaskDefinitionRevision(identity.sourceRevision);
try {
return await findExecutionRevision(
this.pool,
identity.projectId,
identity.taskId,
identity.sourceRevision,
);
} catch (error) {
throw mappedError(error);
}
}
}
/** Administration-only TaskDefinition publisher; execution revisions are separate. */
export class PostgresTaskDefinitionRepository
extends PostgresTaskDefinitionSource
implements TaskDefinitionRepository
{
constructor(
pool: PostgresPool,
private readonly semanticRegistry: TaskSpecSemanticRegistry =
createBuiltInTaskSpecSemanticRegistry(),
) {
super(pool);
}
async appendTaskDefinitionRevision(
input: AppendTaskDefinitionRevisionCommand,
transactionHook?: PostgresTaskDefinitionRevisionTransactionHook,
): Promise<
Readonly<{
status: 'created' | 'updated' | 'existing';
definition: TaskDefinitionRecord;
}>
> {
if (
transactionHook !== undefined &&
typeof transactionHook !== 'function'
) {
throw new InvalidTaskDefinitionError('transaction hook is invalid');
}
const normalized = normalizeAppendTaskDefinitionRevisionCommand(input);
const command = Object.freeze({
...normalized,
spec: this.semanticRegistry.normalize({
projectId: normalized.projectId,
taskId: normalized.taskId,
kind: normalized.kind,
spec: normalized.spec,
}),
});
const compiledExecution =
command.enabled &&
command.kind === 'command' &&
command.spec.schema === BUILT_IN_COMMAND_TASK_SPEC_SCHEMA
? compileClusterCommandTaskDefinition(
createTaskDefinitionRecord(
command,
command.occurredAtMs,
),
this.semanticRegistry,
)
: null;
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch {
throw unavailable();
}
let began = false;
let transactionHookError: unknown;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const ownership = await client.query(
`SELECT 1
FROM "ql3"."plugin_package_task_ownerships"
WHERE project_id = $1 AND task_id = $2`,
[command.projectId, command.taskId],
);
if (ownership.rows.length !== 0) {
throw new TaskDefinitionConflictError();
}
const replay = await findByMutation(client, command.mutationId);
if (replay) {
const expected = createTaskDefinitionRecord(
command,
replay.createdAtMs,
);
if (!sameRecord(replay, expected)) {
throw new TaskDefinitionConflictError();
}
const persistedExecution = await findExecutionRevision(
client,
replay.projectId,
replay.taskId,
replay.revision,
);
if (!sameExecutionRevision(persistedExecution, compiledExecution)) {
throw unavailable();
}
if (transactionHook) {
try {
const hookResult = await transactionHook(
client,
Object.freeze({ command, replay, record: replay }),
);
if (hookResult !== undefined) {
throw new InvalidTaskDefinitionError(
'transaction hook must not return a value',
);
}
} catch (error) {
transactionHookError = error;
throw error;
}
}
await client.query('COMMIT');
began = false;
return Object.freeze({ status: 'existing', definition: replay });
}
const project = await client.query<{ status: unknown }>(
`SELECT status FROM "ql3"."projects"
WHERE id = $1`,
[command.projectId],
);
if (
project.rows.length !== 1 ||
project.rows[0]?.status !== 'active'
) {
throw new TaskDefinitionConflictError();
}
const inserted = await client.query(
`INSERT INTO "ql3"."task_definitions" (
project_id, task_id, current_revision, created_at_ms, updated_at_ms
) VALUES ($1, $2, 1, $3, $3)
ON CONFLICT (project_id, task_id) DO NOTHING
RETURNING task_id`,
[command.projectId, command.taskId, command.occurredAtMs],
);
if (inserted.rows.length > 1) throw unavailable();
const created = inserted.rows.length === 1;
const head = await client.query<TaskDefinitionRow>(
`SELECT current_revision AS "currentRevision",
created_at_ms AS "createdAtMs",
updated_at_ms AS "updatedAtMs"
FROM "ql3"."task_definitions"
WHERE project_id = $1 AND task_id = $2
FOR UPDATE`,
[command.projectId, command.taskId],
);
if (head.rows.length !== 1) throw unavailable();
const currentRevision = postgresRequiredInteger(
head.rows[0]!.currentRevision,
unavailable,
);
const createdAtMs = postgresRequiredInteger(
head.rows[0]!.createdAtMs,
unavailable,
);
const previousUpdatedAtMs = postgresRequiredInteger(
head.rows[0]!.updatedAtMs,
unavailable,
);
if (
(created ? command.expectedRevision !== null :
currentRevision !== command.expectedRevision) ||
command.occurredAtMs < previousUpdatedAtMs
) {
throw new TaskDefinitionConflictError();
}
const definition = createTaskDefinitionRecord(command, createdAtMs);
await client.query(
`INSERT INTO "ql3"."task_definition_revisions" (
project_id, task_id, revision, mutation_id, name, description,
kind, spec_json, labels_json, enabled, content_digest, created_at_ms
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9::jsonb,
$10, $11, $12)`,
[
definition.projectId,
definition.taskId,
definition.revision,
definition.mutationId,
definition.name,
definition.description ?? null,
definition.kind,
JSON.stringify(definition.spec),
JSON.stringify(definition.labels),
definition.enabled,
definition.contentDigest,
definition.updatedAtMs,
],
);
if (compiledExecution) {
await client.query(
`INSERT INTO "ql3"."task_execution_revisions" (
project_id, task_id, source_revision, task_revision,
source_content_digest, executor_type, plan_schema, plan_json,
content_digest, created_at_ms
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10)`,
[
compiledExecution.projectId,
compiledExecution.taskId,
compiledExecution.sourceRevision,
compiledExecution.taskRevision,
compiledExecution.sourceContentDigest,
compiledExecution.executorType,
compiledExecution.planSchema,
JSON.stringify(executionPlanJson(compiledExecution)),
compiledExecution.contentDigest,
compiledExecution.createdAtMs,
],
);
}
if (!created) {
const update = await client.query(
`UPDATE "ql3"."task_definitions"
SET current_revision = $1, updated_at_ms = $2
WHERE project_id = $3 AND task_id = $4
AND current_revision = $5`,
[
definition.revision,
definition.updatedAtMs,
definition.projectId,
definition.taskId,
command.expectedRevision,
],
);
if (update.rowCount !== 1) throw new TaskDefinitionConflictError();
}
if (transactionHook) {
try {
const hookResult = await transactionHook(
client,
Object.freeze({ command, replay: null, record: definition }),
);
if (hookResult !== undefined) {
throw new InvalidTaskDefinitionError(
'transaction hook must not return a value',
);
}
} catch (error) {
transactionHookError = error;
throw error;
}
}
await client.query('COMMIT');
began = false;
return Object.freeze({
status: created ? 'created' : 'updated',
definition,
});
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
state &&
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
) {
continue;
}
if (error === transactionHookError && error instanceof Error) {
throw error;
}
throw mappedError(error);
} finally {
client.release();
}
}
throw unavailable();
}
}
@@ -0,0 +1,145 @@
import { X509Certificate } from 'node:crypto';
import { closeSync, fstatSync, openSync, readSync, constants } from 'node:fs';
import { isAbsolute } from 'node:path';
export const POSTGRES_CA_MAX_FILE_BYTES = 256 * 1024;
export const POSTGRES_CA_MAX_CERTIFICATES = 16;
export const POSTGRES_CA_FILE_ERROR_CODES = Object.freeze([
'QL3_POSTGRES_CA_INVALID_PATH',
'QL3_POSTGRES_CA_UNAVAILABLE',
'QL3_POSTGRES_CA_NOT_REGULAR',
'QL3_POSTGRES_CA_INSECURE_PERMISSIONS',
'QL3_POSTGRES_CA_INVALID_SIZE',
'QL3_POSTGRES_CA_CHANGED_DURING_READ',
'QL3_POSTGRES_CA_INVALID_ENCODING',
'QL3_POSTGRES_CA_INVALID_PEM',
'QL3_POSTGRES_CA_TOO_MANY_CERTIFICATES',
'QL3_POSTGRES_CA_NOT_CA',
'QL3_POSTGRES_CA_DUPLICATE_CERTIFICATE',
] as const);
export type PostgresCertificateAuthorityFileErrorCode =
(typeof POSTGRES_CA_FILE_ERROR_CODES)[number];
export class PostgresCertificateAuthorityFileError extends TypeError {
constructor(readonly code: PostgresCertificateAuthorityFileErrorCode) {
super('PostgreSQL certificate authority file is invalid');
this.name = 'PostgresCertificateAuthorityFileError';
}
}
export interface PostgresCertificateAuthorityFileInspection {
readonly bundle: string;
readonly fingerprints256: readonly string[];
}
function fail(code: PostgresCertificateAuthorityFileErrorCode): never {
throw new PostgresCertificateAuthorityFileError(code);
}
function readBoundedRegularFile(filePath: string): Buffer {
if (
typeof filePath !== 'string' ||
filePath.length < 1 ||
filePath.length > 4096 ||
/[\0\r\n]/.test(filePath) ||
!isAbsolute(filePath)
) {
fail('QL3_POSTGRES_CA_INVALID_PATH');
}
let descriptor: number;
try {
descriptor = openSync(filePath, constants.O_RDONLY);
} catch {
fail('QL3_POSTGRES_CA_UNAVAILABLE');
}
try {
const stat = fstatSync(descriptor);
if (!stat.isFile()) fail('QL3_POSTGRES_CA_NOT_REGULAR');
if ((stat.mode & 0o022) !== 0) {
fail('QL3_POSTGRES_CA_INSECURE_PERMISSIONS');
}
if (
!Number.isSafeInteger(stat.size) ||
stat.size < 1 ||
stat.size > POSTGRES_CA_MAX_FILE_BYTES
) {
fail('QL3_POSTGRES_CA_INVALID_SIZE');
}
const bytes = Buffer.alloc(stat.size + 1);
let offset = 0;
while (offset < bytes.byteLength) {
const count = readSync(
descriptor,
bytes,
offset,
bytes.byteLength - offset,
null,
);
if (count === 0) break;
offset += count;
}
if (offset !== stat.size) {
fail('QL3_POSTGRES_CA_CHANGED_DURING_READ');
}
return bytes.subarray(0, offset);
} finally {
closeSync(descriptor);
}
}
/**
* Loads one immutable PostgreSQL trust bundle. Kubernetes projected Secret
* symlinks are accepted because the opened target is validated with fstat.
*/
export function inspectPostgresCertificateAuthorityFile(
filePath: string,
): PostgresCertificateAuthorityFileInspection {
const bytes = readBoundedRegularFile(filePath);
let source: string;
try {
source = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
} catch {
fail('QL3_POSTGRES_CA_INVALID_ENCODING');
} finally {
bytes.fill(0);
}
const pattern =
/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g;
const blocks = source.match(pattern) ?? [];
if (blocks.length < 1 || source.replace(pattern, '').trim().length !== 0) {
fail('QL3_POSTGRES_CA_INVALID_PEM');
}
if (blocks.length > POSTGRES_CA_MAX_CERTIFICATES) {
fail('QL3_POSTGRES_CA_TOO_MANY_CERTIFICATES');
}
const fingerprints = new Set<string>();
const normalized: string[] = [];
for (const block of blocks) {
let certificate: X509Certificate;
try {
certificate = new X509Certificate(block);
} catch {
fail('QL3_POSTGRES_CA_INVALID_PEM');
}
if (!certificate.ca) fail('QL3_POSTGRES_CA_NOT_CA');
if (fingerprints.has(certificate.fingerprint256)) {
fail('QL3_POSTGRES_CA_DUPLICATE_CERTIFICATE');
}
fingerprints.add(certificate.fingerprint256);
normalized.push(certificate.toString().trimEnd());
}
return Object.freeze({
bundle: `${normalized.join('\n')}\n`,
fingerprints256: Object.freeze([...fingerprints].sort()),
});
}
export function loadPostgresCertificateAuthorityFile(filePath: string): string {
return inspectPostgresCertificateAuthorityFile(filePath).bundle;
}
@@ -0,0 +1,142 @@
import type { PostgresConnectionOptions } from './pool';
export type PostgresConnectionEnvironment = Readonly<
Record<string, string | undefined>
>;
export interface PostgresConnectionEnvironmentKeys {
readonly connectionString: string;
readonly host: string;
readonly port: string;
readonly database: string;
readonly user: string;
readonly password: string;
}
export class PostgresConnectionEnvironmentError extends TypeError {
readonly code = 'QL3_POSTGRES_CONNECTION_ENVIRONMENT_INVALID';
constructor(message: string) {
super(`PostgreSQL connection environment is invalid: ${message}`);
this.name = 'PostgresConnectionEnvironmentError';
}
}
function present(value: string | undefined): boolean {
return value !== undefined && value !== '';
}
function bounded(
environment: PostgresConnectionEnvironment,
name: string,
maximumLength: number,
): string | undefined {
const value = environment[name];
if (!present(value)) return undefined;
if (value!.length > maximumLength || /[\0\r\n]/.test(value!)) {
throw new PostgresConnectionEnvironmentError(`${name} is invalid`);
}
return value;
}
function assertConnectionString(value: string, name: string): void {
let url: URL;
try {
url = new URL(value);
} catch {
throw new PostgresConnectionEnvironmentError(`${name} must be a valid URL`);
}
if (url.protocol !== 'postgres:' && url.protocol !== 'postgresql:') {
throw new PostgresConnectionEnvironmentError(
`${name} must use postgres or postgresql`,
);
}
for (const parameter of url.searchParams.keys()) {
if (parameter.toLowerCase().startsWith('ssl')) {
throw new PostgresConnectionEnvironmentError(
'PostgreSQL TLS query parameters are forbidden',
);
}
}
}
function port(value: string | undefined, name: string): number {
if (value === undefined) return 5432;
if (!/^\d+$/.test(value)) {
throw new PostgresConnectionEnvironmentError(`${name} must be an integer`);
}
const parsed = Number(value);
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 65_535) {
throw new PostgresConnectionEnvironmentError(
`${name} must be between 1 and 65535`,
);
}
return parsed;
}
function identifier(value: string | undefined, name: string): string {
if (value === undefined || !/^[A-Za-z_][A-Za-z0-9_$]{0,62}$/.test(value)) {
throw new PostgresConnectionEnvironmentError(
`${name} must be a safe PostgreSQL identifier`,
);
}
return value;
}
/**
* Loads either one legacy connection URL or an exact discrete credential set.
*
* Discrete values avoid duplicating Kubernetes Secret passwords inside a DSN
* and let an operator-managed DatabaseRole Secret remain the sole password
* authority. The two forms are deliberately mutually exclusive.
*/
export function loadPostgresConnectionEnvironment(
environment: PostgresConnectionEnvironment,
keys: PostgresConnectionEnvironmentKeys,
): PostgresConnectionOptions {
if (
!environment ||
typeof environment !== 'object' ||
Array.isArray(environment)
) {
throw new PostgresConnectionEnvironmentError(
'environment must be an object',
);
}
const connectionString = bounded(environment, keys.connectionString, 4096);
const host = bounded(environment, keys.host, 253);
const portValue = bounded(environment, keys.port, 5);
const database = bounded(environment, keys.database, 63);
const user = bounded(environment, keys.user, 63);
const password = bounded(environment, keys.password, 1024);
const discreteValues = [host, portValue, database, user, password];
const hasDiscreteValue = discreteValues.some((value) => value !== undefined);
if (connectionString !== undefined) {
if (hasDiscreteValue) {
throw new PostgresConnectionEnvironmentError(
`${keys.connectionString} cannot be combined with discrete PostgreSQL connection keys`,
);
}
assertConnectionString(connectionString, keys.connectionString);
return Object.freeze({ connectionString });
}
if (
host === undefined ||
database === undefined ||
user === undefined ||
password === undefined
) {
throw new PostgresConnectionEnvironmentError(
`use ${keys.connectionString} or provide ${keys.host}, ${keys.database}, ${keys.user}, and ${keys.password}`,
);
}
return Object.freeze({
host,
port: port(portValue, keys.port),
database: identifier(database, keys.database),
user: identifier(user, keys.user),
password,
});
}
@@ -0,0 +1,548 @@
import {
Pool,
type PoolClient,
type PoolConfig,
type QueryResultRow,
} from 'pg';
import { isIP } from 'node:net';
import type {
PostgresClient,
PostgresDatabaseResource,
PostgresPool,
PostgresQueryResult,
} from '@qinglong/runtime-core';
const DEFAULT_RUNTIME_APPLICATION_NAME = 'qinglong-cluster-runtime';
const DEFAULT_ADMIN_APPLICATION_NAME = 'qinglong-cluster-admin';
const DEFAULT_AI_CREDENTIAL_TESTER_APPLICATION_NAME =
'qinglong-ai-credential-tester';
const DEFAULT_AUTOMATION_MANAGER_APPLICATION_NAME =
'qinglong-automation-manager';
const DEFAULT_APPROVAL_MANAGER_APPLICATION_NAME = 'qinglong-approval-manager';
const DEFAULT_PACKAGE_MANAGER_APPLICATION_NAME = 'qinglong-package-manager';
const DEFAULT_PACKAGE_EXECUTOR_APPLICATION_NAME = 'qinglong-package-executor';
const DEFAULT_WORKER_CREDENTIAL_MANAGER_APPLICATION_NAME =
'qinglong-worker-credential-manager';
const DEFAULT_WORKER_CREDENTIAL_EXECUTOR_APPLICATION_NAME =
'qinglong-worker-credential-executor';
const DEFAULT_WORKER_INGRESS_APPLICATION_NAME = 'qinglong-worker-ingress';
const DEFAULT_MIGRATION_APPLICATION_NAME = 'qinglong-cluster-migration';
const DEFAULT_RUNTIME_POOL_SIZE = 8;
const DEFAULT_ADMIN_POOL_SIZE = 2;
const DEFAULT_WORKER_INGRESS_POOL_SIZE = 4;
const DEFAULT_MIGRATION_POOL_SIZE = 1;
const MAX_RUNTIME_POOL_SIZE = 64;
const MAX_ADMIN_POOL_SIZE = 4;
const MAX_WORKER_INGRESS_POOL_SIZE = 16;
const MAX_MIGRATION_POOL_SIZE = 4;
const DEFAULT_CONNECTION_TIMEOUT_MS = 5_000;
const DEFAULT_IDLE_TIMEOUT_MS = 30_000;
const DEFAULT_MAX_LIFETIME_SECONDS = 300;
const DEFAULT_RUNTIME_STATEMENT_TIMEOUT_MS = 5_000;
const DEFAULT_MIGRATION_STATEMENT_TIMEOUT_MS = 15_000;
const DEFAULT_RUNTIME_LOCK_TIMEOUT_MS = 1_000;
const DEFAULT_MIGRATION_LOCK_TIMEOUT_MS = 5_000;
const DEFAULT_RUNTIME_IDLE_TRANSACTION_TIMEOUT_MS = 10_000;
const DEFAULT_MIGRATION_IDLE_TRANSACTION_TIMEOUT_MS = 15_000;
const FIXED_SEARCH_PATH = '-c search_path=ql3,pg_catalog';
export const POSTGRES_AVAILABILITY_SQLSTATE_CLASSES = Object.freeze([
'08',
] as const);
export const POSTGRES_AVAILABILITY_SQLSTATES = Object.freeze([
'08000',
'08001',
'08003',
'08004',
'08006',
'08007',
'08P01',
'25006',
'57P01',
'57P02',
'57P03',
'57P04',
] as const);
export const POSTGRES_AVAILABILITY_SYSTEM_ERROR_CODES = Object.freeze([
'EAI_AGAIN',
'ECONNREFUSED',
'ECONNRESET',
'EHOSTUNREACH',
'ENETUNREACH',
'ENOTFOUND',
'EPIPE',
'ETIMEDOUT',
] as const);
const POSTGRES_AVAILABILITY_CODES = new Set<string>([
...POSTGRES_AVAILABILITY_SQLSTATES,
...POSTGRES_AVAILABILITY_SYSTEM_ERROR_CODES,
]);
export type PostgresDatabaseRole =
| 'runtime'
| 'ai-maintenance'
| 'ai-credential-manager'
| 'ai-credential-tester'
| 'admin'
| 'automation-manager'
| 'approval-manager'
| 'package-manager'
| 'package-executor'
| 'worker-credential-manager'
| 'worker-credential-executor'
| 'worker-ingress'
| 'migration';
export type PostgresTlsOptions =
| Readonly<{
mode: 'verify-full';
ca?: string;
servername: string;
}>
| Readonly<{
mode: 'disable';
}>;
export interface PostgresConnectionOptions {
readonly connectionString?: string;
readonly host?: string;
readonly port?: number;
readonly database?: string;
readonly user?: string;
readonly password?: string | (() => string | Promise<string>);
readonly tls?: PostgresTlsOptions;
}
export interface PostgresPoolOptions {
readonly applicationName?: string;
readonly maxConnections?: number;
readonly connectionTimeoutMs?: number;
readonly idleTimeoutMs?: number;
readonly maxLifetimeSeconds?: number;
readonly keepAliveInitialDelayMs?: number;
}
export interface OpenPostgresDatabaseOptions {
readonly role: PostgresDatabaseRole;
readonly connection: PostgresConnectionOptions;
readonly pool?: PostgresPoolOptions;
readonly onPoolError: (error: Error) => void;
}
export function isPostgresTlsDnsServername(value: unknown): value is string {
if (
typeof value !== 'string' ||
value.length < 3 ||
value.length > 253 ||
isIP(value) !== 0
) {
return false;
}
const labels = value.split('.');
return (
labels.length >= 2 &&
labels.every((label) =>
/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/.test(label),
)
);
}
export type QingLongPostgresQueryResult<
TRow extends Record<string, unknown> = Record<string, unknown>,
> = PostgresQueryResult<TRow>;
export type QingLongPostgresClient = PostgresClient;
export type QingLongPostgresPool = PostgresPool;
export type QingLongPostgresDatabaseResource = PostgresDatabaseResource;
export function isPostgresAvailabilityError(error: unknown): error is Error {
if (!(error instanceof Error)) return false;
const code = (error as Error & { readonly code?: unknown }).code;
if (typeof code !== 'string') return false;
return (
POSTGRES_AVAILABILITY_CODES.has(code) ||
(code.length === 5 &&
POSTGRES_AVAILABILITY_SQLSTATE_CLASSES.some((prefix) =>
code.startsWith(prefix),
))
);
}
function reportPostgresAvailabilityError(
error: unknown,
listener: (error: Error) => void,
force = false,
): void {
if (!(error instanceof Error)) return;
if (!force && !isPostgresAvailabilityError(error)) return;
try {
listener(error);
} catch {
// Availability reporting must never replace the original query/Pool error.
}
}
function requireIntegerInRange(
name: string,
value: number,
minimum: number,
maximum: number,
): number {
if (!Number.isInteger(value) || value < minimum || value > maximum) {
throw new RangeError(
`${name} must be an integer between ${minimum} and ${maximum}`,
);
}
return value;
}
function validateApplicationName(value: string): string {
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,62}$/.test(value)) {
throw new TypeError(
'PostgreSQL applicationName must contain 1-63 safe identifier characters',
);
}
return value;
}
function assertNoConnectionStringTlsOverrides(connectionString: string): void {
let parsed: URL;
try {
parsed = new URL(connectionString);
} catch {
throw new TypeError('PostgreSQL connectionString must be a valid URL');
}
if (parsed.protocol !== 'postgres:' && parsed.protocol !== 'postgresql:') {
throw new TypeError(
'PostgreSQL connectionString must use postgres or postgresql',
);
}
for (const key of parsed.searchParams.keys()) {
if (key.toLowerCase().startsWith('ssl')) {
throw new TypeError(
'PostgreSQL TLS settings must use the explicit tls option',
);
}
}
}
function tlsConfig(tls: PostgresTlsOptions | undefined): PoolConfig['ssl'] {
if (tls?.mode === 'disable') return false;
if (tls !== undefined && !isPostgresTlsDnsServername(tls.servername)) {
throw new TypeError(
'PostgreSQL verify-full TLS requires an explicit DNS servername',
);
}
return {
rejectUnauthorized: true,
...(tls?.ca === undefined ? {} : { ca: tls.ca }),
...(tls === undefined ? {} : { servername: tls.servername }),
};
}
function buildPoolConfig(options: OpenPostgresDatabaseOptions): PoolConfig {
if (
![
'runtime',
'ai-maintenance',
'ai-credential-manager',
'ai-credential-tester',
'admin',
'automation-manager',
'approval-manager',
'package-manager',
'package-executor',
'worker-credential-manager',
'worker-credential-executor',
'worker-ingress',
'migration',
].includes(options.role)
) {
throw new TypeError('PostgreSQL database role is invalid');
}
const connection = options.connection;
const hasConnectionString = connection.connectionString !== undefined;
const hasDiscreteAddress =
connection.host !== undefined ||
connection.port !== undefined ||
connection.database !== undefined ||
connection.user !== undefined;
if (hasConnectionString && hasDiscreteAddress) {
throw new TypeError(
'PostgreSQL connectionString cannot be combined with host, port, database, or user',
);
}
if (hasConnectionString) {
assertNoConnectionStringTlsOverrides(connection.connectionString!);
} else if (
connection.host === undefined ||
connection.database === undefined ||
connection.user === undefined
) {
throw new TypeError(
'PostgreSQL connection requires connectionString or host, database, and user',
);
}
const isRuntime = options.role === 'runtime';
const isAiMaintenance = options.role === 'ai-maintenance';
const isAiCredentialManager = options.role === 'ai-credential-manager';
const isAiCredentialTester = options.role === 'ai-credential-tester';
const isAdmin = options.role === 'admin';
const isAutomationManager = options.role === 'automation-manager';
const isApprovalManager = options.role === 'approval-manager';
const isPackageManager = options.role === 'package-manager';
const isPackageExecutor = options.role === 'package-executor';
const isWorkerCredentialManager =
options.role === 'worker-credential-manager';
const isWorkerCredentialExecutor =
options.role === 'worker-credential-executor';
const isShortLivedAuthority =
isAiMaintenance ||
isAiCredentialManager ||
isAiCredentialTester ||
isAdmin ||
isAutomationManager ||
isApprovalManager ||
isPackageManager ||
isPackageExecutor ||
isWorkerCredentialManager ||
isWorkerCredentialExecutor;
const isWorkerIngress = options.role === 'worker-ingress';
const maximumPoolSize = isRuntime
? MAX_RUNTIME_POOL_SIZE
: isShortLivedAuthority
? MAX_ADMIN_POOL_SIZE
: isWorkerIngress
? MAX_WORKER_INGRESS_POOL_SIZE
: MAX_MIGRATION_POOL_SIZE;
const maxConnections = requireIntegerInRange(
'PostgreSQL maxConnections',
options.pool?.maxConnections ??
(isRuntime
? DEFAULT_RUNTIME_POOL_SIZE
: isShortLivedAuthority
? DEFAULT_ADMIN_POOL_SIZE
: isWorkerIngress
? DEFAULT_WORKER_INGRESS_POOL_SIZE
: DEFAULT_MIGRATION_POOL_SIZE),
1,
maximumPoolSize,
);
const connectionTimeoutMs = requireIntegerInRange(
'PostgreSQL connectionTimeoutMs',
options.pool?.connectionTimeoutMs ?? DEFAULT_CONNECTION_TIMEOUT_MS,
100,
60_000,
);
const idleTimeoutMs = requireIntegerInRange(
'PostgreSQL idleTimeoutMs',
options.pool?.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS,
1_000,
300_000,
);
const maxLifetimeSeconds = requireIntegerInRange(
'PostgreSQL maxLifetimeSeconds',
options.pool?.maxLifetimeSeconds ?? DEFAULT_MAX_LIFETIME_SECONDS,
30,
3_600,
);
const keepAliveInitialDelayMs = requireIntegerInRange(
'PostgreSQL keepAliveInitialDelayMs',
options.pool?.keepAliveInitialDelayMs ?? 1_000,
0,
60_000,
);
const applicationName = validateApplicationName(
options.pool?.applicationName ??
(isRuntime
? DEFAULT_RUNTIME_APPLICATION_NAME
: isAdmin
? DEFAULT_ADMIN_APPLICATION_NAME
: isAiCredentialTester
? DEFAULT_AI_CREDENTIAL_TESTER_APPLICATION_NAME
: isAutomationManager
? DEFAULT_AUTOMATION_MANAGER_APPLICATION_NAME
: isApprovalManager
? DEFAULT_APPROVAL_MANAGER_APPLICATION_NAME
: isPackageManager
? DEFAULT_PACKAGE_MANAGER_APPLICATION_NAME
: isPackageExecutor
? DEFAULT_PACKAGE_EXECUTOR_APPLICATION_NAME
: isWorkerCredentialManager
? DEFAULT_WORKER_CREDENTIAL_MANAGER_APPLICATION_NAME
: isWorkerCredentialExecutor
? DEFAULT_WORKER_CREDENTIAL_EXECUTOR_APPLICATION_NAME
: isWorkerIngress
? DEFAULT_WORKER_INGRESS_APPLICATION_NAME
: DEFAULT_MIGRATION_APPLICATION_NAME),
);
return {
...(connection.connectionString === undefined
? {
host: connection.host,
...(connection.port === undefined
? {}
: {
port: requireIntegerInRange(
'PostgreSQL port',
connection.port,
1,
65_535,
),
}),
database: connection.database,
user: connection.user,
}
: { connectionString: connection.connectionString }),
...(connection.password === undefined
? {}
: { password: connection.password }),
ssl: tlsConfig(connection.tls),
application_name: applicationName,
options: FIXED_SEARCH_PATH,
max: maxConnections,
min: 0,
connectionTimeoutMillis: connectionTimeoutMs,
idleTimeoutMillis: idleTimeoutMs,
maxLifetimeSeconds,
keepAlive: true,
keepAliveInitialDelayMillis: keepAliveInitialDelayMs,
allowExitOnIdle: false,
statement_timeout:
isRuntime || isWorkerIngress
? DEFAULT_RUNTIME_STATEMENT_TIMEOUT_MS
: DEFAULT_MIGRATION_STATEMENT_TIMEOUT_MS,
query_timeout:
isRuntime || isWorkerIngress
? DEFAULT_RUNTIME_STATEMENT_TIMEOUT_MS + 1_000
: DEFAULT_MIGRATION_STATEMENT_TIMEOUT_MS + 1_000,
lock_timeout:
isRuntime || isWorkerIngress
? DEFAULT_RUNTIME_LOCK_TIMEOUT_MS
: DEFAULT_MIGRATION_LOCK_TIMEOUT_MS,
idle_in_transaction_session_timeout:
isRuntime || isWorkerIngress
? DEFAULT_RUNTIME_IDLE_TRANSACTION_TIMEOUT_MS
: DEFAULT_MIGRATION_IDLE_TRANSACTION_TIMEOUT_MS,
};
}
async function query<TRow extends Record<string, unknown>>(
queryable: Pick<Pool | PoolClient, 'query'>,
text: string,
values?: readonly unknown[],
onAvailabilityError: (error: Error) => void = () => {},
): Promise<PostgresQueryResult<TRow>> {
try {
const result = await queryable.query<TRow & QueryResultRow>(
text,
values === undefined ? undefined : [...values],
);
return { rows: result.rows, rowCount: result.rowCount };
} catch (error) {
reportPostgresAvailabilityError(error, onAvailabilityError);
throw error;
}
}
class PgClientBinding implements PostgresClient {
private readonly onClientError: (error: Error) => void;
constructor(
private readonly client: PoolClient,
private readonly onAvailabilityError: (error: Error) => void,
) {
this.onClientError = (error) =>
reportPostgresAvailabilityError(error, this.onAvailabilityError, true);
this.client.on('error', this.onClientError);
}
query<TRow extends Record<string, unknown> = Record<string, unknown>>(
text: string,
values?: readonly unknown[],
): Promise<PostgresQueryResult<TRow>> {
return query<TRow>(this.client, text, values, this.onAvailabilityError);
}
release(): void {
this.client.removeListener('error', this.onClientError);
this.client.release();
}
}
export class PgPoolBinding implements PostgresPool {
constructor(
private readonly driverPool: Pool,
private readonly onAvailabilityError: (error: Error) => void = () => {},
) {}
query<TRow extends Record<string, unknown> = Record<string, unknown>>(
text: string,
values?: readonly unknown[],
): Promise<PostgresQueryResult<TRow>> {
return query<TRow>(this.driverPool, text, values, this.onAvailabilityError);
}
async connect(): Promise<PostgresClient> {
try {
return new PgClientBinding(
await this.driverPool.connect(),
this.onAvailabilityError,
);
} catch (error) {
reportPostgresAvailabilityError(error, this.onAvailabilityError);
throw error;
}
}
}
class PgDatabaseResource implements PostgresDatabaseResource {
readonly pool: PostgresPool;
private closePromise: Promise<void> | undefined;
constructor(
private readonly driverPool: Pool,
onAvailabilityError: (error: Error) => void,
) {
this.pool = new PgPoolBinding(driverPool, onAvailabilityError);
}
close(): Promise<void> {
this.closePromise ??= this.driverPool.end();
return this.closePromise;
}
}
/**
* Builds a lazy database opener for the core cluster bootstrap. Constructing
* the opener, and invoking it, do not acquire a PostgreSQL connection; the
* first readiness or repository query triggers pg.Pool connection creation.
*/
export function createPostgresDatabaseOpener(
options: OpenPostgresDatabaseOptions,
): () => Promise<PostgresDatabaseResource> {
const config = buildPoolConfig(options);
return async () => {
const driverPool = new Pool(config);
driverPool.on('connect', (client) => {
// Keep one listener for the physical connection lifetime. pg swaps its
// own idle listener while a client is checked out, and a failover can
// otherwise emit a second socket error after a checkout binding has
// released but before Pool teardown has completed.
client.on('error', (error) =>
reportPostgresAvailabilityError(error, options.onPoolError, true),
);
});
driverPool.on('error', (error) =>
reportPostgresAvailabilityError(error, options.onPoolError, true),
);
return new PgDatabaseResource(driverPool, options.onPoolError);
};
}
@@ -0,0 +1,42 @@
export {
PgPoolBinding,
createPostgresDatabaseOpener,
isPostgresTlsDnsServername,
type OpenPostgresDatabaseOptions,
type PostgresConnectionOptions,
type PostgresDatabaseRole,
type PostgresPoolOptions,
type PostgresTlsOptions,
type QingLongPostgresClient,
type QingLongPostgresDatabaseResource,
type QingLongPostgresPool,
type QingLongPostgresQueryResult,
} from '../connection/pool';
export {
POSTGRES_SCHEMA_READINESS_ERROR_CODES,
PostgresSchemaReadinessError,
assertPostgresAdminSchemaReady,
type PostgresSchemaReadinessErrorCode,
type PostgresSchemaReadinessReport,
} from '../schema/schemaReadiness';
export { postgresqlMainMigrationManifest } from '../migration/migrationManifest';
export {
loadPostgresConnectionEnvironment,
type PostgresConnectionEnvironment,
type PostgresConnectionEnvironmentKeys,
} from '../connection/connectionEnvironment';
export {
loadPostgresCertificateAuthorityFile,
type PostgresCertificateAuthorityFileInspection,
} from '../connection/certificateAuthority';
export * from '../security/identityAdministrationRepository';
export * from '../security/apiCredentialAdministrationRepository';
export * from '../security/securityAuditQueryRepository';
export * from '../worker-credential/workerCredentialAdministrationRepository';
export * from '../automation/automationAdministrationRepository';
export { PostgresTaskDefinitionRepository } from '../automation/taskDefinitionRepository';
export { PostgresTriggerRepository } from '../scheduling/triggerRepository';
export { PostgresToolResultKeyCatalogRepository } from '../tool-execution/toolResultKeyCatalogRepository';
export { PostgresToolResultRekeyRepository } from '../tool-execution/toolResultRekeyRepository';
@@ -0,0 +1,21 @@
export {
PgPoolBinding,
createPostgresDatabaseOpener,
isPostgresTlsDnsServername,
type OpenPostgresDatabaseOptions,
type PostgresConnectionOptions,
type PostgresDatabaseRole,
type PostgresPoolOptions,
type PostgresTlsOptions,
} from '../connection/pool';
export {
PostgresConnectionEnvironmentError,
loadPostgresConnectionEnvironment,
type PostgresConnectionEnvironment,
type PostgresConnectionEnvironmentKeys,
} from '../connection/connectionEnvironment';
export {
loadPostgresCertificateAuthorityFile,
type PostgresCertificateAuthorityFileInspection,
} from '../connection/certificateAuthority';
export { PostgresProjectPolicyRepository } from '../security/projectPolicyRepository';
@@ -0,0 +1,20 @@
export {
PgPoolBinding,
createPostgresDatabaseOpener,
isPostgresTlsDnsServername,
type OpenPostgresDatabaseOptions,
type PostgresConnectionOptions,
type PostgresDatabaseRole,
type PostgresPoolOptions,
type PostgresTlsOptions,
} from '../connection/pool';
export {
PostgresConnectionEnvironmentError,
loadPostgresConnectionEnvironment,
type PostgresConnectionEnvironment,
type PostgresConnectionEnvironmentKeys,
} from '../connection/connectionEnvironment';
export {
loadPostgresCertificateAuthorityFile,
type PostgresCertificateAuthorityFileInspection,
} from '../connection/certificateAuthority';
@@ -0,0 +1,21 @@
export {
PgPoolBinding,
createPostgresDatabaseOpener,
isPostgresTlsDnsServername,
type OpenPostgresDatabaseOptions,
type PostgresConnectionOptions,
type PostgresPoolOptions,
type QingLongPostgresDatabaseResource,
type QingLongPostgresPool,
} from '../connection/pool';
export {
PostgresConnectionEnvironmentError,
loadPostgresConnectionEnvironment,
type PostgresConnectionEnvironment,
} from '../connection/connectionEnvironment';
export {
PostgresCertificateAuthorityFileError,
loadPostgresCertificateAuthorityFile,
} from '../connection/certificateAuthority';
@@ -0,0 +1,36 @@
export * from '../automation/automationAdministrationRepository';
export {
PgPoolBinding,
createPostgresDatabaseOpener,
isPostgresTlsDnsServername,
type OpenPostgresDatabaseOptions,
type PostgresConnectionOptions,
type PostgresDatabaseRole,
type PostgresPoolOptions,
type PostgresTlsOptions,
} from '../connection/pool';
export {
PostgresConnectionEnvironmentError,
loadPostgresConnectionEnvironment,
type PostgresConnectionEnvironment,
type PostgresConnectionEnvironmentKeys,
} from '../connection/connectionEnvironment';
export {
loadPostgresCertificateAuthorityFile,
type PostgresCertificateAuthorityFileInspection,
} from '../connection/certificateAuthority';
export {
PostgresTaskDefinitionSource,
PostgresTaskExecutionRevisionSource,
} from '../automation/taskDefinitionRepository';
export { PostgresTriggerSource } from '../scheduling/triggerRepository';
export { PostgresProjectPolicyRepository } from '../security/projectPolicyRepository';
export {
PostgresPluginPackageIdentityKeysetLedgerRepository as PostgresAutomationManagementIdentityKeysetLedgerRepository,
type ClusterManagementIdentityAuthority,
} from '../management/pluginPackageIdentityKeysetLedgerRepository';
export {
assertPostgresAutomationManagerSchemaReady,
PostgresSchemaReadinessError,
type PostgresSchemaReadinessReport,
} from '../schema/schemaReadiness';
@@ -0,0 +1,76 @@
export {
PgPoolBinding,
createPostgresDatabaseOpener,
isPostgresTlsDnsServername,
type OpenPostgresDatabaseOptions,
type PostgresConnectionOptions,
type PostgresDatabaseRole,
type PostgresPoolOptions,
type PostgresTlsOptions,
type QingLongPostgresClient,
type QingLongPostgresDatabaseResource,
type QingLongPostgresPool,
type QingLongPostgresQueryResult,
} from '../connection/pool';
export {
PostgresConnectionEnvironmentError,
loadPostgresConnectionEnvironment,
type PostgresConnectionEnvironment,
type PostgresConnectionEnvironmentKeys,
} from '../connection/connectionEnvironment';
export {
POSTGRES_CA_FILE_ERROR_CODES,
POSTGRES_CA_MAX_CERTIFICATES,
POSTGRES_CA_MAX_FILE_BYTES,
PostgresCertificateAuthorityFileError,
inspectPostgresCertificateAuthorityFile,
loadPostgresCertificateAuthorityFile,
type PostgresCertificateAuthorityFileErrorCode,
type PostgresCertificateAuthorityFileInspection,
} from '../connection/certificateAuthority';
export {
POSTGRES_SCHEMA_READINESS_ERROR_CODES,
PostgresSchemaReadinessError,
assertPostgresPackageExecutorSchemaReady,
type PostgresSchemaReadinessErrorCode,
type PostgresSchemaReadinessReport,
} from '../schema/schemaReadiness';
export { PostgresPluginPackageMaterializedRevisionRepository } from '../plugin-package/installation/pluginPackageMaterializedRevisionRepository';
export { PostgresPluginPackageAutomationPublicationRepository } from '../plugin-package/publication/pluginPackageAutomationPublicationRepository';
export {
CLUSTER_PLUGIN_PACKAGE_QUARANTINE_TARGET_LIMIT,
PostgresPluginPackageQuarantineRepository,
} from '../plugin-package/lifecycle/pluginPackageQuarantineRepository';
export { PostgresPluginPackageLifecycleRepository } from '../plugin-package/lifecycle/pluginPackageLifecycleRepository';
export {
PostgresPluginPackageLifecyclePlanReader,
PostgresPluginPackageLifecyclePlanRepository,
} from '../plugin-package/lifecycle/pluginPackageLifecyclePlanRepository';
export {
POSTGRES_PLUGIN_PACKAGE_PROVENANCE_RECOVERY_PAGE_LIMIT,
PostgresPluginPackagePublisherProvenanceRepository,
type PluginPackagePublisherProvenanceRecoveryCursor,
type PluginPackagePublisherProvenanceRecoveryPage,
} from '../plugin-package/publisher/pluginPackagePublisherProvenanceRepository';
export { PostgresPluginPackagePublisherTrustAuthorityRepository } from '../plugin-package/publisher/pluginPackagePublisherTrustAuthorityRepository';
export {
PostgresPluginPackagePublisherRevocationProposalRepository,
findPostgresPluginPackagePublisherRevocationProposal,
} from '../plugin-package/publisher/pluginPackagePublisherRevocationProposalRepository';
export {
PostgresPluginPackagePublisherTrustTransitionProposalRepository,
findPostgresPluginPackagePublisherTrustTransitionProposal,
} from '../plugin-package/publisher/pluginPackagePublisherTrustTransitionProposalRepository';
export {
PostgresPluginPackagePublisherTrustTransitionRepository,
type ApplyPostgresPluginPackagePublisherTrustTransitionInput,
type ApplyPostgresPluginPackagePublisherTrustTransitionResult,
} from '../plugin-package/publisher/pluginPackagePublisherTrustTransitionRepository';
export { PostgresPluginPackageTaskReconciliationRepository } from '../plugin-package/publication/pluginPackageTaskReconciliationRepository';
export { PostgresProjectToolDefinitionSnapshotRepository } from '../tool-execution/projectToolDefinitionSnapshotRepository';
export { postgresqlMainMigrationManifest } from '../migration/migrationManifest';
@@ -0,0 +1,61 @@
export {
PgPoolBinding,
createPostgresDatabaseOpener,
isPostgresTlsDnsServername,
type OpenPostgresDatabaseOptions,
type PostgresConnectionOptions,
type PostgresDatabaseRole,
type PostgresPoolOptions,
type PostgresTlsOptions,
type QingLongPostgresClient,
type QingLongPostgresDatabaseResource,
type QingLongPostgresPool,
type QingLongPostgresQueryResult,
} from '../connection/pool';
export {
PostgresConnectionEnvironmentError,
loadPostgresConnectionEnvironment,
type PostgresConnectionEnvironment,
type PostgresConnectionEnvironmentKeys,
} from '../connection/connectionEnvironment';
export {
POSTGRES_CA_FILE_ERROR_CODES,
POSTGRES_CA_MAX_CERTIFICATES,
POSTGRES_CA_MAX_FILE_BYTES,
PostgresCertificateAuthorityFileError,
inspectPostgresCertificateAuthorityFile,
loadPostgresCertificateAuthorityFile,
type PostgresCertificateAuthorityFileErrorCode,
type PostgresCertificateAuthorityFileInspection,
} from '../connection/certificateAuthority';
export {
POSTGRES_SCHEMA_READINESS_ERROR_CODES,
PostgresSchemaReadinessError,
assertPostgresPackageManagerSchemaReady,
type PostgresSchemaReadinessErrorCode,
type PostgresSchemaReadinessReport,
} from '../schema/schemaReadiness';
export { postgresqlMainMigrationManifest } from '../migration/migrationManifest';
export {
PostgresPluginPackageManagementQuotaRepository,
type PostgresPluginPackageManagementQuotaOptions,
} from '../management/pluginPackageManagementQuotaRepository';
export {
PostgresPluginPackageIdentityKeysetLedgerConflictError,
PostgresPluginPackageIdentityKeysetLedgerRepository,
PostgresPluginPackageIdentityKeysetLedgerUnavailableError,
type PluginPackageIdentityKeysetLedgerPort,
type PluginPackageIdentityKeysetLedgerSnapshot,
} from '../management/pluginPackageIdentityKeysetLedgerRepository';
export { PostgresPluginPackagePublisherTrustAuthorityRepository } from '../plugin-package/publisher/pluginPackagePublisherTrustAuthorityRepository';
export { PostgresPluginPackageLifecyclePlanReader } from '../plugin-package/lifecycle/pluginPackageLifecyclePlanRepository';
export { PostgresPluginPackageInstallInventoryReader } from '../plugin-package/installation/pluginPackageInstallRepository';
export { PostgresPluginPackagePublisherRevocationProposalRepository } from '../plugin-package/publisher/pluginPackagePublisherRevocationProposalRepository';
export { PostgresPluginPackagePublisherTrustTransitionProposalRepository } from '../plugin-package/publisher/pluginPackagePublisherTrustTransitionProposalRepository';
@@ -0,0 +1,82 @@
export {
POSTGRES_CA_FILE_ERROR_CODES,
POSTGRES_CA_MAX_CERTIFICATES,
POSTGRES_CA_MAX_FILE_BYTES,
PostgresCertificateAuthorityFileError,
inspectPostgresCertificateAuthorityFile,
loadPostgresCertificateAuthorityFile,
type PostgresCertificateAuthorityFileErrorCode,
type PostgresCertificateAuthorityFileInspection,
} from '../connection/certificateAuthority';
export {
PgPoolBinding,
POSTGRES_AVAILABILITY_SQLSTATE_CLASSES,
POSTGRES_AVAILABILITY_SQLSTATES,
POSTGRES_AVAILABILITY_SYSTEM_ERROR_CODES,
createPostgresDatabaseOpener,
isPostgresTlsDnsServername,
isPostgresAvailabilityError,
type OpenPostgresDatabaseOptions,
type PostgresConnectionOptions,
type PostgresDatabaseRole,
type PostgresPoolOptions,
type PostgresTlsOptions,
type QingLongPostgresClient,
type QingLongPostgresDatabaseResource,
type QingLongPostgresPool,
type QingLongPostgresQueryResult,
} from '../connection/pool';
export {
PostgresConnectionEnvironmentError,
loadPostgresConnectionEnvironment,
type PostgresConnectionEnvironment,
type PostgresConnectionEnvironmentKeys,
} from '../connection/connectionEnvironment';
export {
POSTGRES_SCHEMA_READINESS_ERROR_CODES,
PostgresSchemaReadinessError,
assertPostgresSchemaReady,
type PostgresSchemaReadinessErrorCode,
type PostgresSchemaReadinessReport,
} from '../schema/schemaReadiness';
export { postgresqlMainMigrationManifest } from '../migration/migrationManifest';
export { PostgresToolInvocationArtifactRepository } from '../tool-execution/toolInvocationArtifactRepository';
export { PostgresProjectToolDefinitionSnapshotRepository } from '../tool-execution/projectToolDefinitionSnapshotRepository';
export { PostgresStepRunRepository } from '../run/stepRunRepository';
export { PostgresToolExecutionStartBarrierRepository } from '../tool-execution/toolExecutionStartBarrierRepository';
export { PostgresToolExecutionCompletionRepository } from '../tool-execution/toolExecutionCompletionRepository';
export { PostgresToolExecutionFailureCompletionRepository } from '../tool-execution/toolExecutionFailureCompletionRepository';
export { PostgresToolResultKeyCatalogReader } from '../tool-execution/toolResultKeyCatalogRepository';
export { PostgresToolResultRekeyReader } from '../tool-execution/toolResultRekeyRepository';
export * from '../run/runRepository';
export * from '../security/projectPolicyRepository';
export * from '../security/apiCredentialRepository';
export * from '../security/securityAuditRepository';
export * from '../run-recovery/clusterRecoverySource';
export * from '../run-recovery/clusterRecoveryClaimRepository';
export * from '../run-recovery/clusterRecoveryResolutionRepository';
export * from '../run-recovery/clusterRuntimeRecoverySource';
export * from '../run-recovery/clusterRunLostRetryRepository';
export * from '../remote-execution/workerSessionRepository';
export * from '../remote-execution/runDispatchLeaseRepository';
export * from '../remote-execution/remoteRunActivationRepository';
export * from '../worker-credential/remoteWorkerSecretDeliveryRepository';
export * from '../remote-execution/remoteWorkerCompletionRepository';
export * from '../remote-execution/remoteWorkerLeaseControlRepository';
export * from '../run-recovery/clusterRunCancellationRepository';
export * from '../run-recovery/clusterRunCancellationConvergenceRepository';
export * from '../remote-execution/clusterDispatchRepository';
export * from '../worker-credential/workerCredentialRepository';
export * from '../remote-execution/workerExecutionAttestationRepository';
export * from '../remote-execution/remoteWorkerAttestationEvidenceProvider';
export * from '../scheduling/clusterScheduleRepository';
export {
PostgresTaskDefinitionSource,
PostgresTaskExecutionRevisionSource,
} from '../automation/taskDefinitionRepository';
export { PostgresTriggerSource } from '../scheduling/triggerRepository';
@@ -0,0 +1,27 @@
export {
PgPoolBinding,
createPostgresDatabaseOpener,
type OpenPostgresDatabaseOptions,
type PostgresConnectionOptions,
type PostgresDatabaseRole,
type PostgresPoolOptions,
type PostgresTlsOptions,
type QingLongPostgresClient,
type QingLongPostgresDatabaseResource,
type QingLongPostgresPool,
type QingLongPostgresQueryResult,
} from '../connection/pool';
export {
POSTGRES_SCHEMA_READINESS_ERROR_CODES,
PostgresSchemaReadinessError,
assertPostgresWorkerIngressSchemaReady,
type PostgresSchemaReadinessErrorCode,
type PostgresSchemaReadinessReport,
} from '../schema/schemaReadiness';
export { postgresqlMainMigrationManifest } from '../migration/migrationManifest';
export * from '../worker-credential/workerCredentialRepository';
export * from '../remote-execution/workerSessionRepository';
export * from '../remote-execution/workerExecutionAttestationRepository';
export * from '../security/securityAuditRepository';
+124
View File
@@ -0,0 +1,124 @@
export {
PgPoolBinding,
POSTGRES_AVAILABILITY_SQLSTATE_CLASSES,
POSTGRES_AVAILABILITY_SQLSTATES,
POSTGRES_AVAILABILITY_SYSTEM_ERROR_CODES,
createPostgresDatabaseOpener,
isPostgresAvailabilityError,
type OpenPostgresDatabaseOptions,
type PostgresConnectionOptions,
type PostgresDatabaseRole,
type PostgresPoolOptions,
type PostgresTlsOptions,
type QingLongPostgresClient,
type QingLongPostgresDatabaseResource,
type QingLongPostgresPool,
type QingLongPostgresQueryResult,
} from './connection/pool';
export {
ql3PostgresTables,
ql3Schema,
apiCredentialMutations,
approvedActionExecutions,
pluginPackageInstallProposals,
pluginPackageIdentityKeysetLedger,
pluginPackageManagementQuotaBuckets,
pluginPackageMaterializedRevisions,
pluginPackageQuarantineEvents,
pluginPackageWorkflowAdmissions,
pluginPackageWorkflowAdmissionSteps,
pluginPackageWithdrawalReceipts,
pluginPackageWithdrawalTasks,
identitySubjectMutations,
apiCredentials,
identitySubjects,
projectRoleBindings,
projects,
taskDefinitions,
taskDefinitionRevisions,
taskExecutionRevisions,
triggers,
triggerRevisions,
triggerSchedules,
runAttempts,
runDispatchLeases,
runRecoveryControls,
runEvents,
runRetryPolicies,
runs,
schemaCapabilities,
schemaMigrations,
securityAuditEvents,
toolExecutionAuditReceipts,
toolExecutionTraceAnchors,
workerSessions,
workerCredentials,
workerCredentialMutations,
workerExecutionAttestations,
} from './schema/schema';
export {
runPostgresMigrations,
type RunPostgresMigrationsOptions,
} from './migration/migrate';
export {
POSTGRESQL_MAIN_MIGRATION_STREAM_ID,
POSTGRESQL_MIGRATION_HISTORY_TABLE,
POSTGRESQL_MIGRATION_SCHEMA,
PostgresMigrationLeaderUnavailableError,
PostgresMigrationStreamStore,
readPostgresMigrationHistory,
type PostgresMigrationClient,
type PostgresMigrationContext,
type PostgresMigrationPool,
type PostgresMigrationQueryable,
type PostgresMigrationQueryResult,
} from './migrations/postgresMigrationStreamStore';
export { postgresqlMainMigrationStream } from './migrations';
export { postgresqlMainMigrationManifest } from './migration/migrationManifest';
export { PostgresToolInvocationArtifactRepository } from './tool-execution/toolInvocationArtifactRepository';
export {
postgresqlControlSchemaContract,
type PostgresSchemaContract,
type PostgresSchemaContractTable,
} from './schema/schemaContract';
export {
POSTGRES_SCHEMA_READINESS_ERROR_CODES,
PostgresSchemaReadinessError,
assertPostgresSchemaReady,
assertPostgresWorkerIngressSchemaReady,
type PostgresSchemaReadinessErrorCode,
type PostgresSchemaReadinessReport,
} from './schema/schemaReadiness';
export * from './run/runRepository';
export * from './security/projectPolicyRepository';
export * from './security/apiCredentialRepository';
export * from './security/securityAuditRepository';
export * from './run-recovery/clusterRecoverySource';
export * from './run-recovery/clusterRecoveryClaimRepository';
export * from './run-recovery/clusterRecoveryResolutionRepository';
export * from './run-recovery/clusterRuntimeRecoverySource';
export * from './run-recovery/clusterRunLostRetryRepository';
export * from './remote-execution/workerSessionRepository';
export * from './remote-execution/runDispatchLeaseRepository';
export * from './remote-execution/remoteRunActivationRepository';
export * from './worker-credential/remoteWorkerSecretDeliveryRepository';
export * from './remote-execution/remoteWorkerCompletionRepository';
export * from './remote-execution/remoteWorkerLeaseControlRepository';
export * from './run-recovery/clusterRunCancellationRepository';
export * from './run-recovery/clusterRunCancellationConvergenceRepository';
export * from './remote-execution/clusterDispatchRepository';
export * from './worker-credential/workerCredentialRepository';
export * from './remote-execution/workerExecutionAttestationRepository';
export * from './remote-execution/remoteWorkerAttestationEvidenceProvider';
export * from './scheduling/clusterScheduleRepository';
export * from './worker-credential/workerCredentialAdministrationRepository';
export * from './automation/taskDefinitionRepository';
export * from './scheduling/triggerRepository';
export * from './automation/automationAdministrationRepository';
@@ -0,0 +1,306 @@
// PostgreSQL anti-rollback ledger shared by bounded Cluster management authorities.
import type { PostgresClient, PostgresPool } from '@qinglong/runtime-core';
export type ClusterManagementIdentityAuthority =
| 'plugin-package-management'
| 'worker-credential-management'
| 'automation-management'
| 'approval-management';
const DIGEST_PATTERN = /^[A-Za-z0-9_-]{43}$/;
const KEY_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
type Row = Record<string, unknown>;
export interface PluginPackageIdentityKeysetLedgerSnapshot {
readonly schemaVersion: 1;
readonly generation: number;
readonly digest: string;
readonly issuer: string;
readonly audience: string;
readonly activeKeyIds: readonly string[];
readonly revokedKeyIds: readonly string[];
}
export interface PluginPackageIdentityKeysetLedgerPort {
observe(
snapshot: Readonly<PluginPackageIdentityKeysetLedgerSnapshot>,
): Promise<void>;
}
export class PostgresPluginPackageIdentityKeysetLedgerConflictError extends Error {
readonly code = 'POSTGRES_PLUGIN_PACKAGE_IDENTITY_KEYSET_LEDGER_CONFLICT';
constructor() {
super('PostgreSQL Plugin Package identity keyset ledger conflicts');
this.name = 'PostgresPluginPackageIdentityKeysetLedgerConflictError';
}
}
export class PostgresPluginPackageIdentityKeysetLedgerUnavailableError extends Error {
readonly code = 'POSTGRES_PLUGIN_PACKAGE_IDENTITY_KEYSET_LEDGER_UNAVAILABLE';
constructor(options?: ErrorOptions) {
super(
'PostgreSQL Plugin Package identity keyset ledger is unavailable',
options,
);
this.name = 'PostgresPluginPackageIdentityKeysetLedgerUnavailableError';
}
}
function reviewedKeyIds(
value: readonly string[],
minimum: number,
maximum: number,
): readonly string[] {
if (
!Array.isArray(value) ||
value.length < minimum ||
value.length > maximum
) {
throw new TypeError(
'PostgreSQL Plugin Package identity key ids are invalid',
);
}
const seen = new Set<string>();
for (const keyId of value) {
if (
typeof keyId !== 'string' ||
!KEY_ID_PATTERN.test(keyId) ||
seen.has(keyId)
) {
throw new TypeError(
'PostgreSQL Plugin Package identity key ids are invalid',
);
}
seen.add(keyId);
}
return Object.freeze([...value].sort());
}
function reviewedSnapshot(
value: Readonly<PluginPackageIdentityKeysetLedgerSnapshot>,
): Readonly<PluginPackageIdentityKeysetLedgerSnapshot> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
Object.keys(value).length !== 7 ||
Object.keys(value).some(
(key) =>
![
'schemaVersion',
'generation',
'digest',
'issuer',
'audience',
'activeKeyIds',
'revokedKeyIds',
].includes(key),
) ||
value.schemaVersion !== 1 ||
!Number.isSafeInteger(value.generation) ||
value.generation < 1 ||
typeof value.digest !== 'string' ||
!DIGEST_PATTERN.test(value.digest) ||
typeof value.issuer !== 'string' ||
value.issuer.length < 1 ||
value.issuer.length > 512 ||
CONTROL_PATTERN.test(value.issuer) ||
typeof value.audience !== 'string' ||
value.audience.length < 1 ||
value.audience.length > 256 ||
CONTROL_PATTERN.test(value.audience)
) {
throw new TypeError(
'PostgreSQL Plugin Package identity keyset snapshot is invalid',
);
}
const activeKeyIds = reviewedKeyIds(value.activeKeyIds, 1, 8);
const revokedKeyIds = reviewedKeyIds(value.revokedKeyIds, 0, 64);
if (activeKeyIds.some((keyId) => revokedKeyIds.includes(keyId))) {
throw new TypeError(
'PostgreSQL Plugin Package identity keyset snapshot is invalid',
);
}
return Object.freeze({ ...value, activeKeyIds, revokedKeyIds });
}
function integer(row: Row, name: string): number {
const value = Number(row[name]);
if (!Number.isSafeInteger(value) || value < 1) {
throw new Error(`invalid ${name}`);
}
return value;
}
function text(row: Row, name: string): string {
const value = row[name];
if (typeof value !== 'string') throw new Error(`invalid ${name}`);
return value;
}
function textArray(row: Row, name: string): readonly string[] {
const value = row[name];
if (
!Array.isArray(value) ||
value.some((candidate) => typeof candidate !== 'string')
) {
throw new Error(`invalid ${name}`);
}
return value;
}
function sameArray(left: readonly string[], right: readonly string[]): boolean {
return (
left.length === right.length &&
left.every((value, index) => value === right[index])
);
}
function includesAll(
candidate: readonly string[],
required: readonly string[],
): boolean {
const values = new Set(candidate);
return required.every((value) => values.has(value));
}
async function rollback(client: PostgresClient): Promise<void> {
await client.query('ROLLBACK').catch(() => undefined);
}
export class PostgresPluginPackageIdentityKeysetLedgerRepository
implements PluginPackageIdentityKeysetLedgerPort
{
readonly #authority: ClusterManagementIdentityAuthority;
constructor(
private readonly pool: PostgresPool,
authority: ClusterManagementIdentityAuthority = 'plugin-package-management',
) {
if (!pool || typeof pool.connect !== 'function') {
throw new TypeError(
'PostgreSQL Plugin Package identity keyset ledger pool is invalid',
);
}
if (
authority !== 'plugin-package-management' &&
authority !== 'worker-credential-management' &&
authority !== 'automation-management' &&
authority !== 'approval-management'
) {
throw new TypeError(
'PostgreSQL management identity keyset authority is invalid',
);
}
this.#authority = authority;
}
async observe(
value: Readonly<PluginPackageIdentityKeysetLedgerSnapshot>,
): Promise<void> {
const candidate = reviewedSnapshot(value);
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
throw new PostgresPluginPackageIdentityKeysetLedgerUnavailableError({
cause: error,
});
}
try {
await client.query('BEGIN');
await client.query(
`INSERT INTO "ql3"."plugin_package_identity_keyset_ledger" (
authority, generation, digest, issuer, audience,
active_key_ids, revoked_key_ids, updated_at_ms
)
VALUES (
$1, $2, $3, $4, $5, $6::jsonb, $7::jsonb,
floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint
)
ON CONFLICT (authority) DO NOTHING`,
[
this.#authority,
candidate.generation,
candidate.digest,
candidate.issuer,
candidate.audience,
JSON.stringify(candidate.activeKeyIds),
JSON.stringify(candidate.revokedKeyIds),
],
);
const selected = await client.query<Row>(
`SELECT generation, digest, issuer, audience,
active_key_ids AS "activeKeyIds",
revoked_key_ids AS "revokedKeyIds"
FROM "ql3"."plugin_package_identity_keyset_ledger"
WHERE authority = $1
FOR UPDATE`,
[this.#authority],
);
if (selected.rows.length !== 1) throw new Error('ledger row is missing');
const current = selected.rows[0]!;
const generation = integer(current, 'generation');
const digest = text(current, 'digest');
const issuer = text(current, 'issuer');
const audience = text(current, 'audience');
const activeKeyIds = textArray(current, 'activeKeyIds');
const revokedKeyIds = textArray(current, 'revokedKeyIds');
const exact =
generation === candidate.generation &&
digest === candidate.digest &&
issuer === candidate.issuer &&
audience === candidate.audience &&
sameArray(activeKeyIds, candidate.activeKeyIds) &&
sameArray(revokedKeyIds, candidate.revokedKeyIds);
if (!exact) {
const retained = [
...candidate.activeKeyIds,
...candidate.revokedKeyIds,
];
if (
candidate.generation <= generation ||
candidate.issuer !== issuer ||
candidate.audience !== audience ||
!includesAll(candidate.revokedKeyIds, revokedKeyIds) ||
!includesAll(retained, activeKeyIds)
) {
throw new PostgresPluginPackageIdentityKeysetLedgerConflictError();
}
await client.query(
`UPDATE "ql3"."plugin_package_identity_keyset_ledger"
SET generation = $2,
digest = $3,
active_key_ids = $4::jsonb,
revoked_key_ids = $5::jsonb,
updated_at_ms =
floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint
WHERE authority = $1`,
[
this.#authority,
candidate.generation,
candidate.digest,
JSON.stringify(candidate.activeKeyIds),
JSON.stringify(candidate.revokedKeyIds),
],
);
}
await client.query('COMMIT');
} catch (error) {
await rollback(client);
if (
error instanceof PostgresPluginPackageIdentityKeysetLedgerConflictError
) {
throw error;
}
throw new PostgresPluginPackageIdentityKeysetLedgerUnavailableError({
cause: error,
});
} finally {
client.release();
}
}
}
@@ -0,0 +1,321 @@
// PostgreSQL distributed quota authority for authenticated management operations.
import type { PostgresPool } from '@qinglong/runtime-core';
import {
PLUGIN_PACKAGE_MANAGEMENT_QUOTA_OPERATIONS,
PluginPackageManagementQuotaExceededError,
PluginPackageManagementUnavailableError,
type ConsumePluginPackageManagementQuotaCommand,
type PluginPackageManagementQuotaOperation,
type PluginPackageManagementQuotaPort,
type PluginPackageManagementQuotaResult,
} from '@qinglong/runtime-core/plugin-package-management';
import {
postgresRequiredBoolean,
postgresRequiredInteger,
} from '../repository/definitionRepositorySupport';
const DEFAULT_WINDOW_MS = 60_000;
const DEFAULT_LIMITS = Object.freeze({
'plugin-package.propose': 30,
'plugin-package.decide': 60,
'plugin-package.inspect': 600,
});
const PROJECT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$/;
type Row = Record<string, unknown>;
export interface PostgresPluginPackageManagementQuotaOptions {
readonly windowMs?: number;
readonly limits?: Partial<
Readonly<Record<PluginPackageManagementQuotaOperation, number>>
>;
}
function unavailable(cause?: unknown): PluginPackageManagementUnavailableError {
return new PluginPackageManagementUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function integer(
value: number | undefined,
fallback: number,
minimum: number,
maximum: number,
label: string,
): number {
const candidate = value ?? fallback;
if (
!Number.isSafeInteger(candidate) ||
candidate < minimum ||
candidate > maximum
) {
throw new TypeError(`PostgreSQL Package management ${label} is invalid`);
}
return candidate;
}
function reviewedOptions(
value: PostgresPluginPackageManagementQuotaOptions | undefined,
): Readonly<{
windowMs: number;
limits: Readonly<Record<PluginPackageManagementQuotaOperation, number>>;
}> {
if (
value !== undefined &&
(!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
Object.keys(value).some((key) => key !== 'windowMs' && key !== 'limits'))
) {
throw new TypeError(
'PostgreSQL Package management quota options are invalid',
);
}
const limits = value?.limits;
if (
limits !== undefined &&
(!limits ||
typeof limits !== 'object' ||
Array.isArray(limits) ||
Object.keys(limits).some(
(key) =>
!PLUGIN_PACKAGE_MANAGEMENT_QUOTA_OPERATIONS.includes(
key as PluginPackageManagementQuotaOperation,
),
))
) {
throw new TypeError(
'PostgreSQL Package management quota limits are invalid',
);
}
return Object.freeze({
windowMs: integer(
value?.windowMs,
DEFAULT_WINDOW_MS,
1_000,
5 * 60_000,
'quota window',
),
limits: Object.freeze({
'plugin-package.propose': integer(
limits?.['plugin-package.propose'],
DEFAULT_LIMITS['plugin-package.propose'],
1,
1_000,
'proposal quota',
),
'plugin-package.decide': integer(
limits?.['plugin-package.decide'],
DEFAULT_LIMITS['plugin-package.decide'],
1,
1_000,
'decision quota',
),
'plugin-package.inspect': integer(
limits?.['plugin-package.inspect'],
DEFAULT_LIMITS['plugin-package.inspect'],
1,
1_000,
'inspection quota',
),
}),
});
}
function validateCommand(
command: ConsumePluginPackageManagementQuotaCommand,
): Readonly<ConsumePluginPackageManagementQuotaCommand> {
if (
!command ||
typeof command !== 'object' ||
Array.isArray(command) ||
Object.keys(command).some(
(key) =>
!['projectId', 'subject', 'operation', 'idempotencyKey'].includes(key),
) ||
Object.keys(command).length !== 4 ||
typeof command.projectId !== 'string' ||
!PROJECT_PATTERN.test(command.projectId) ||
!command.subject ||
typeof command.subject !== 'object' ||
Array.isArray(command.subject) ||
Object.keys(command.subject).length !== 2 ||
command.subject.type !== 'user' ||
typeof command.subject.id !== 'string' ||
command.subject.id.length < 1 ||
command.subject.id.length > 255 ||
/[\u0000-\u001f\u007f]/.test(command.subject.id) ||
!PLUGIN_PACKAGE_MANAGEMENT_QUOTA_OPERATIONS.includes(command.operation) ||
typeof command.idempotencyKey !== 'string' ||
!IDENTIFIER_PATTERN.test(command.idempotencyKey)
) {
throw new TypeError(
'PostgreSQL Package management quota command is invalid',
);
}
return command;
}
export class PostgresPluginPackageManagementQuotaRepository
implements PluginPackageManagementQuotaPort
{
readonly #windowMs: number;
readonly #limits: Readonly<
Record<PluginPackageManagementQuotaOperation, number>
>;
constructor(
private readonly pool: PostgresPool,
options?: PostgresPluginPackageManagementQuotaOptions,
) {
if (!pool || typeof pool.query !== 'function') {
throw new TypeError(
'PostgreSQL Package management quota pool is invalid',
);
}
const reviewed = reviewedOptions(options);
this.#windowMs = reviewed.windowMs;
this.#limits = reviewed.limits;
}
async consume(
commandValue: ConsumePluginPackageManagementQuotaCommand,
): Promise<Readonly<PluginPackageManagementQuotaResult>> {
const command = validateCommand(commandValue);
const limit = this.#limits[command.operation];
let result;
try {
result = await this.pool.query<Row>(
`
WITH database_clock AS (
SELECT floor(
extract(epoch FROM clock_timestamp()) * 1000
)::bigint AS now_ms
)
INSERT INTO "ql3"."plugin_package_management_quota_buckets" (
project_id, subject_type, subject_id, operation,
window_started_at_ms, consumed_count, receipt_ids, updated_at_ms
)
SELECT
$1, $2, $3, $4,
(now_ms / $6::bigint) * $6::bigint,
1,
jsonb_build_array($5::text),
now_ms
FROM database_clock
ON CONFLICT (project_id, subject_type, subject_id, operation)
DO UPDATE SET
window_started_at_ms = CASE
WHEN "plugin_package_management_quota_buckets".window_started_at_ms
+ $6::bigint <= EXCLUDED.updated_at_ms
THEN (EXCLUDED.updated_at_ms / $6::bigint) * $6::bigint
ELSE "plugin_package_management_quota_buckets".window_started_at_ms
END,
consumed_count = CASE
WHEN "plugin_package_management_quota_buckets".window_started_at_ms
+ $6::bigint <= EXCLUDED.updated_at_ms
THEN 1
WHEN "plugin_package_management_quota_buckets".receipt_ids ? $5::text
THEN "plugin_package_management_quota_buckets".consumed_count
ELSE "plugin_package_management_quota_buckets".consumed_count + 1
END,
receipt_ids = CASE
WHEN "plugin_package_management_quota_buckets".window_started_at_ms
+ $6::bigint <= EXCLUDED.updated_at_ms
THEN jsonb_build_array($5::text)
WHEN "plugin_package_management_quota_buckets".receipt_ids ? $5::text
THEN "plugin_package_management_quota_buckets".receipt_ids
ELSE "plugin_package_management_quota_buckets".receipt_ids
|| jsonb_build_array($5::text)
END,
updated_at_ms = EXCLUDED.updated_at_ms
WHERE
"plugin_package_management_quota_buckets".window_started_at_ms
+ $6::bigint <= EXCLUDED.updated_at_ms
OR "plugin_package_management_quota_buckets".receipt_ids ? $5::text
OR "plugin_package_management_quota_buckets".consumed_count < $7::integer
RETURNING
true AS admitted,
consumed_count AS "consumedCount",
window_started_at_ms + $6::bigint AS "resetAtMs",
updated_at_ms AS "observedAtMs"
`.trim(),
[
command.projectId,
command.subject.type,
command.subject.id,
command.operation,
command.idempotencyKey,
this.#windowMs,
limit,
],
);
} catch (error) {
throw unavailable(error);
}
if (result.rows.length === 0) {
try {
result = await this.pool.query<Row>(
`
WITH database_clock AS (
SELECT floor(
extract(epoch FROM clock_timestamp()) * 1000
)::bigint AS now_ms
)
SELECT
false AS admitted,
buckets.consumed_count AS "consumedCount",
buckets.window_started_at_ms + $5::bigint AS "resetAtMs",
database_clock.now_ms AS "observedAtMs"
FROM "ql3"."plugin_package_management_quota_buckets" AS buckets
CROSS JOIN database_clock
WHERE buckets.project_id = $1
AND buckets.subject_type = $2
AND buckets.subject_id = $3
AND buckets.operation = $4
LIMIT 2
`.trim(),
[
command.projectId,
command.subject.type,
command.subject.id,
command.operation,
this.#windowMs,
],
);
} catch (error) {
throw unavailable(error);
}
}
if (result.rows.length !== 1) throw unavailable();
const row = result.rows[0]!;
const admitted = postgresRequiredBoolean(row.admitted, unavailable);
const consumedCount = postgresRequiredInteger(
row.consumedCount,
unavailable,
);
const resetAtMs = postgresRequiredInteger(row.resetAtMs, unavailable);
const observedAtMs = postgresRequiredInteger(row.observedAtMs, unavailable);
if (
consumedCount < 1 ||
consumedCount > limit ||
resetAtMs <= observedAtMs ||
resetAtMs > observedAtMs + this.#windowMs
) {
throw unavailable();
}
if (!admitted) {
throw new PluginPackageManagementQuotaExceededError(
Math.max(1, resetAtMs - observedAtMs),
);
}
return Object.freeze({
remaining: limit - consumedCount,
resetAtMs,
observedAtMs,
});
}
}
@@ -0,0 +1,28 @@
import {
runMigrationStream,
type PostgresPool,
type RunMigrationStreamOptions,
} from '@qinglong/runtime-core';
import {
PostgresMigrationStreamStore,
type PostgresMigrationContext,
} from '../migrations/postgresMigrationStreamStore';
import { postgresqlMainMigrationStream } from '../migrations';
export interface RunPostgresMigrationsOptions {
readonly pool: PostgresPool;
readonly clock?: () => number;
readonly logger?: RunMigrationStreamOptions<PostgresMigrationContext>['logger'];
}
/** Executes the reviewed pg-* stream through a migration-role pool. */
export function runPostgresMigrations(
options: RunPostgresMigrationsOptions,
): Promise<void> {
return runMigrationStream({
stream: postgresqlMainMigrationStream,
store: new PostgresMigrationStreamStore(options.pool),
...(options.clock === undefined ? {} : { clock: options.clock }),
...(options.logger === undefined ? {} : { logger: options.logger }),
});
}
@@ -0,0 +1,59 @@
export {
PgPoolBinding,
createPostgresDatabaseOpener,
type OpenPostgresDatabaseOptions,
type PostgresConnectionOptions,
type PostgresDatabaseRole,
type PostgresPoolOptions,
type PostgresTlsOptions,
type QingLongPostgresClient,
type QingLongPostgresDatabaseResource,
type QingLongPostgresPool,
type QingLongPostgresQueryResult,
} from '../connection/pool';
export {
PostgresConnectionEnvironmentError,
loadPostgresConnectionEnvironment,
type PostgresConnectionEnvironment,
type PostgresConnectionEnvironmentKeys,
} from '../connection/connectionEnvironment';
export {
runPostgresMigrations,
type RunPostgresMigrationsOptions,
} from './migrate';
export {
POSTGRESQL_MAIN_MIGRATION_STREAM_ID,
POSTGRESQL_MIGRATION_HISTORY_TABLE,
POSTGRESQL_MIGRATION_SCHEMA,
PostgresMigrationLeaderUnavailableError,
PostgresMigrationStreamStore,
readPostgresMigrationHistory,
type PostgresMigrationClient,
type PostgresMigrationContext,
type PostgresMigrationPool,
type PostgresMigrationQueryable,
type PostgresMigrationQueryResult,
} from '../migrations/postgresMigrationStreamStore';
export { postgresqlMainMigrationStream } from '../migrations';
export { postgresqlMainMigrationManifest } from './migrationManifest';
export {
ql3PostgresTables,
ql3Schema,
runAttempts,
runEvents,
runRetryPolicies,
runs,
schemaCapabilities,
schemaMigrations,
} from '../schema/schema';
export {
postgresqlControlSchemaContract,
type PostgresSchemaContract,
type PostgresSchemaContractTable,
} from '../schema/schemaContract';
@@ -0,0 +1,55 @@
#!/usr/bin/env node
// One-shot migration product entrypoint owned by the migration domain.
import { runPostgresMigrationProcess } from './migrationProcess';
const USAGE = 'Usage: ql3-cluster-migrate';
function failureFact(error: unknown): Readonly<Record<string, unknown>> {
const candidate = error as {
readonly name?: unknown;
readonly code?: unknown;
};
return Object.freeze({
schemaVersion: 1,
component: 'qinglong3-cluster-migration',
event: 'migration_failed',
name:
typeof candidate?.name === 'string' && candidate.name.length <= 128
? candidate.name
: 'Error',
...(typeof candidate?.code === 'string' && candidate.code.length <= 128
? { code: candidate.code }
: {}),
});
}
async function main(argv: readonly string[]): Promise<void> {
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
process.stdout.write(`${USAGE}\n`);
return;
}
if (argv.length !== 0) {
process.stderr.write(
`${JSON.stringify({
code: 'QL3_CLUSTER_MIGRATION_CLI_USAGE_INVALID',
message: USAGE,
})}\n`,
);
process.exitCode = 64;
return;
}
try {
await runPostgresMigrationProcess({
environment: process.env,
emit(record) {
process.stdout.write(`${JSON.stringify(record)}\n`);
},
});
} catch (error) {
process.stderr.write(`${JSON.stringify(failureFact(error))}\n`);
process.exitCode = 1;
}
}
void main(process.argv.slice(2));
@@ -0,0 +1,282 @@
import type { MigrationStreamManifest } from '@qinglong/runtime-core';
import { POSTGRESQL_MAIN_MIGRATION_STREAM_ID } from '../migrations/postgresMigrationStreamStore';
export const postgresqlMainMigrationManifest: MigrationStreamManifest =
Object.freeze({
id: POSTGRESQL_MAIN_MIGRATION_STREAM_ID,
dialect: 'postgresql',
migrationIdScheme: 'postgres-prefixed',
checksumScheme: 'sha256',
migrations: Object.freeze([
Object.freeze({
id: 'pg-0001-schema-capability',
checksum:
'9e3499e3bcdfe3d7b2559e64ea7bbf236a8a11ba32d6a45af131034887d5a8ab',
}),
Object.freeze({
id: 'pg-0002-run-core',
checksum:
'5b59a7f9323746e49c6c321e89007f553a0751f25d16ffd23c3ae37dd87f76e4',
}),
Object.freeze({
id: 'pg-0003-run-retry-policy',
checksum:
'621792cde917cc86809bbebff389443e790bdba60f73d04f7a1dc97a0ebf72db',
}),
Object.freeze({
id: 'pg-0004-project-policy',
checksum:
'715675d1725687438106c01193f9b87368705c89793d1553c7cbfa7ddefc2343',
}),
Object.freeze({
id: 'pg-0005-api-credential-security-audit',
checksum:
'd19134de8639df125a8ee4ae4056d6493bf9be4bb2bd28ba3ea1db2a4bf119df',
}),
Object.freeze({
id: 'pg-0006-identity-credential-administration',
checksum:
'fe2e87ad191a185a6187d1ef350b7fb49bc13cb56bef610669a59838103b2f87',
}),
Object.freeze({
id: 'pg-0007-cluster-recovery-indexes',
checksum:
'e7e20ee7789a90ddf68eacbb25a9a4e7f4101358b4fb7fd10fa1d4f256e4b5dd',
}),
Object.freeze({
id: 'pg-0008-run-recovery-claims',
checksum:
'1f32c1dd83107eb1881e1b0e968c59fb33e0c63bb8d526b6cde6f350c07652db',
}),
Object.freeze({
id: 'pg-0009-worker-session-run-lease',
checksum:
'6be7e5380ca4d71aa6fcbe9170e0c9beb67aa976800ce75167aab44c41ae335b',
}),
Object.freeze({
id: 'pg-0010-worker-ingress-attestation',
checksum:
'bbc1c36d3d8d3d162073988c8abcf94795fded80277a56b7ac6aa48544caf367',
}),
Object.freeze({
id: 'pg-0011-api-credential-pepper-binding',
checksum:
'12e76002c42f8409ca417fbb562e29d407eaa57324567d80605471eb9e0fa7a4',
}),
Object.freeze({
id: 'pg-0012-task-trigger-definitions',
checksum:
'963e99d1aec9de46fd8ec034480f6c78dc904e30fe85d1b3790224c66f628055',
}),
Object.freeze({
id: 'pg-0013-task-execution-revisions',
checksum:
'a09b34cfda9102c1c573479f61a70f39a786ccd8b45bb82b640935ede4cb6301',
}),
Object.freeze({
id: 'pg-0014-cluster-scheduler-admission',
checksum:
'5f58a214fb2321c2193bf1f3c4e231d9bc2116cd56fd8550bbc23c81cdbf566d',
}),
Object.freeze({
id: 'pg-0015-worker-credential-delivery-ledger',
checksum:
'e8bcef07055a748a4858ae502233138da4c0b4f9f2ea3d3a02bd9264da61f4a4',
}),
Object.freeze({
id: 'pg-0016-worker-credential-stage-discard-ledger',
checksum:
'8e749a3c16fc9c995124bb80fbe67bb4de30ad20c0a1591f283a0dbf1ce243a0',
}),
Object.freeze({
id: 'pg-0017-database-role-grants',
checksum:
'3bb01c0f0dcab152c01b6ba374e60f0aaae7e7464d589fc76626d5d8191fe9f0',
}),
Object.freeze({
id: 'pg-0018-plugin-package-installs',
checksum:
'300bd91155c6480f4397f903645022fa980d71fabe592d42584b1402328f5348',
}),
Object.freeze({
id: 'pg-0019-approved-actions',
checksum:
'96fd50dc1e42f7b2f54af670460d7b486f2fa7c6a234ea1debad120436fcd40c',
}),
Object.freeze({
id: 'pg-0020-plugin-package-admission-receipts',
checksum:
'b1ae4af68d274eb4324e5fd521f42bfb99c1a81478cc9f57c9b604cd393ea8cf',
}),
Object.freeze({
id: 'pg-0021-approved-action-executions-and-package-proposals',
checksum:
'6f65ca0dcb25ea56b32e82d40a5a7a6b2be6cf9ed4a48d04107ef01e44f1cd6a',
}),
Object.freeze({
id: 'pg-0022-plugin-package-authority-split',
checksum:
'431c7c454583629a46a45bb88107765ef600185d3606bfedabb0ca0e40138bf6',
}),
Object.freeze({
id: 'pg-0023-plugin-package-management-quota',
checksum:
'9e3d1bd16bcb2712885de8f0a094289a60c20dc0e4ed9d90ed5adcf1b0240380',
}),
Object.freeze({
id: 'pg-0024-plugin-package-identity-keyset-ledger',
checksum:
'0c764e9427632a79033ecd75a600e3788b9f7b9b39f55899d2c9d1e2d033105f',
}),
Object.freeze({
id: 'pg-0025-plugin-package-materialized-revisions',
checksum:
'da6413393e26f369a9f2639dbd7a0b25bbd079f5de3c61e96e74f0fb10953a54',
}),
Object.freeze({
id: 'pg-0026-plugin-package-task-reconciliations',
checksum:
'b46c241d958cbc34c2e6052708dbc13e040abc3db92c58b0d5915f05c322a32f',
}),
Object.freeze({
id: 'pg-0027-project-tool-definition-snapshots',
checksum:
'147b413aa7eee0469c23d1895f4b9136ffa90da999370c9ca9d293e94afe0960',
}),
Object.freeze({
id: 'pg-0028-step-runs',
checksum:
'a5fd40534f582d07ff5851aa3b856a872a3685e8c975725e8eacf0f6a44ec04a',
}),
Object.freeze({
id: 'pg-0029-tool-execution-evidence',
checksum:
'c61683499c5d65ad319b2ce18ec94b001131381e9d118ed56f05626f6f83d5c6',
}),
Object.freeze({
id: 'pg-0030-tool-execution-start-barriers',
checksum:
'dba5dd522bc9cc7d7cce1432d14262c0195637c9f407d21bb6dc93b24a5018a7',
}),
Object.freeze({
id: 'pg-0031-tool-invocation-artifacts',
checksum:
'b95d9953315aed36a204caa26f2aba5fe9b2893f67f036c8c118bd5551a0a641',
}),
Object.freeze({
id: 'pg-0032-tool-execution-artifact-bindings',
checksum:
'9dd8c3ce052124370bfef691e21cc582331ce527b0d69740549d3d440bec4946',
}),
Object.freeze({
id: 'pg-0033-tool-execution-completions',
checksum:
'1ada1837e4473c3e4f27437ede826832cc590aa47f8e8bfa512fa0fadd8dd291',
}),
Object.freeze({
id: 'pg-0034-tool-execution-failure-completions',
checksum:
'77aa1e86f748f59b84cb4546f0c376ca2bf6ee424bf1b5b3b7fe4970b8056c2f',
}),
Object.freeze({
id: 'pg-0035-tool-result-key-catalog',
checksum:
'64af556669d93e5cf20a29f5df3842280e56029188e1c719f30325b4b75b84ff',
}),
Object.freeze({
id: 'pg-0036-tool-result-rekey-overlays',
checksum:
'3d4fb20c52502b8a5609a8242adb17eb0348f7dc7fa65f793d61136d54b5c851',
}),
Object.freeze({
id: 'pg-0037-plugin-package-quarantine',
checksum:
'a388c7e49a9fe6e417fb343e4ce30dd1b774513c726696ac387d6a1f9884bcb2',
}),
Object.freeze({
id: 'pg-0038-plugin-package-publisher-provenance',
checksum:
'faa63ec953c41fa75c25c69fce017c07539080a9e8c321b107d067405db6a4ec',
}),
Object.freeze({
id: 'pg-0039-plugin-package-publisher-trust-authority',
checksum:
'e0ee11727cbc5342bf480e2cc3142971ff256182d56fa960fe4a35f5c3333afe',
}),
Object.freeze({
id: 'pg-0040-plugin-package-publisher-trust-transitions',
checksum:
'c45ab5c687b62555530035a11370e7094f7b5608505b02d3325804cc680c23fb',
}),
Object.freeze({
id: 'pg-0041-plugin-package-lifecycle',
checksum:
'99669c63c891124aa0741586d5e92bd40d7a3ddf322ec7da970790958f554101',
}),
Object.freeze({
id: 'pg-0042-plugin-package-lifecycle-plans',
checksum:
'a242067854f4ee5231c75874fa77dda364da962a2dcaf570b5015ee069818c4b',
}),
Object.freeze({
id: 'pg-0043-plugin-package-automation-publications',
checksum:
'f93d3a1205c12b252ae31da08b401cad570c1d7f1dea48e83901eff4afe214b9',
}),
Object.freeze({
id: 'pg-0044-plugin-package-automation-start-guard',
checksum:
'd4f436769e4845c220196f9a0ee4845cc9dae6b2fcd229486f42e4d87e11effd',
}),
Object.freeze({
id: 'pg-0045-plugin-package-workflow-admissions',
checksum:
'e31520b150eb8991004f773ccd8f4fb9ec2bb559ef0750391f28038aeae43b7e',
}),
Object.freeze({
id: 'pg-0046-plugin-package-workflow-task-attempt-admissions',
checksum:
'2c5316c9b4e60a1601d9ad0346f56261e6f15ee9368044e632154adb6fe45b7b',
}),
Object.freeze({
id: 'pg-0047-worker-credential-management-plans',
checksum:
'2004c64019beb6971d43507160a7bd488baba12eb299fde3681a731800145d6a',
}),
Object.freeze({
id: 'pg-0048-worker-credential-preapproved-activation',
checksum:
'cf37300e40a127b08c118c0bbb352035937af935455709fe5569d458ef91df8b',
}),
Object.freeze({
id: 'pg-0049-worker-credential-execution-receipts',
checksum:
'd606e51a326b50f8969942c83b3d06d3ed4aa94618fbc5b25005006016918af9',
}),
Object.freeze({
id: 'pg-0050-worker-credential-management-boundary',
checksum:
'9555dd93ab076bf450cfdf16e2178586a2c58738359266fa5d5345c938913555',
}),
Object.freeze({
id: 'pg-0051-automation-management-boundary',
checksum:
'1f5e2beff59570163cee7ba2f798a149b5108522bc9db05ecb387d99cc0f96f5',
}),
Object.freeze({
id: 'pg-0052-automation-management-identity-keyset-ledger',
checksum:
'8594427ad2d53caf61891d21296c140f43ca8204cf728c66784c6db48b7743cd',
}),
Object.freeze({
id: 'pg-0053-plugin-package-workflow-run-list-index',
checksum:
'1848d8ffcf930462a0beab1220860ac6a626228511a0f55e997077bcb6ef4b63',
}),
Object.freeze({
id: 'pg-0054-approval-management-boundary',
checksum:
'5e3e6b222269f095e0d7a985fdeb0ea154510e59dfe15873192af8c8d603fca3',
}),
]),
});
@@ -0,0 +1,260 @@
import type {
OpenPostgresDatabase,
PostgresDatabaseResource,
PostgresPool,
} from '@qinglong/runtime-core';
import {
createPostgresDatabaseOpener,
isPostgresTlsDnsServername,
type PostgresConnectionOptions,
type PostgresPoolOptions,
} from '../connection/pool';
import { loadPostgresConnectionEnvironment } from '../connection/connectionEnvironment';
import { loadPostgresCertificateAuthorityFile } from '../connection/certificateAuthority';
import { runPostgresMigrations } from './migrate';
export type PostgresMigrationProcessEnvironment = Readonly<
Record<string, string | undefined>
>;
export interface PostgresMigrationProcessConfig {
readonly connection: PostgresConnectionOptions;
readonly pool: PostgresPoolOptions;
}
export interface PostgresMigrationProcessEvent {
readonly schemaVersion: 1;
readonly component: 'qinglong3-cluster-migration';
readonly event: 'migration_started' | 'migration_completed';
}
export interface RunPostgresMigrationProcessOptions {
readonly environment: PostgresMigrationProcessEnvironment;
readonly openDatabase?: OpenPostgresDatabase;
readonly migrate?: (options: {
readonly pool: PostgresPool;
}) => Promise<void>;
readonly emit?: (
event: PostgresMigrationProcessEvent,
) => void | Promise<void>;
}
export class PostgresMigrationProcessConfigError extends TypeError {
readonly code = 'QL3_POSTGRES_MIGRATION_PROCESS_CONFIG_INVALID';
constructor(message: string) {
super(`PostgreSQL migration process configuration is invalid: ${message}`);
this.name = 'PostgresMigrationProcessConfigError';
}
}
function boundedValue(
environment: PostgresMigrationProcessEnvironment,
name: string,
maximumLength: number,
required = false,
): string | undefined {
const value = environment[name];
if (value === undefined || value === '') {
if (required) {
throw new PostgresMigrationProcessConfigError(`${name} is required`);
}
return undefined;
}
if (value.length > maximumLength || /[\0\r\n]/.test(value)) {
throw new PostgresMigrationProcessConfigError(`${name} is invalid`);
}
return value;
}
function booleanValue(
environment: PostgresMigrationProcessEnvironment,
name: string,
): boolean {
const value = environment[name];
if (value === undefined || value === '') return false;
if (value === 'true') return true;
if (value === 'false') return false;
throw new PostgresMigrationProcessConfigError(
`${name} must be true or false`,
);
}
/** Parses a migration-only PostgreSQL connection without reading runtime keys. */
export function loadPostgresMigrationProcessConfig(
environment: PostgresMigrationProcessEnvironment,
): PostgresMigrationProcessConfig {
if (
!environment ||
typeof environment !== 'object' ||
Array.isArray(environment)
) {
throw new PostgresMigrationProcessConfigError(
'environment must be an object',
);
}
let connection: PostgresConnectionOptions;
try {
connection = loadPostgresConnectionEnvironment(environment, {
connectionString: 'QL3_POSTGRES_MIGRATION_URL',
host: 'QL3_POSTGRES_MIGRATION_HOST',
port: 'QL3_POSTGRES_MIGRATION_PORT',
database: 'QL3_POSTGRES_MIGRATION_DATABASE',
user: 'QL3_POSTGRES_MIGRATION_USER',
password: 'QL3_POSTGRES_MIGRATION_PASSWORD',
});
} catch (error) {
throw new PostgresMigrationProcessConfigError(
error instanceof Error
? error.message
: 'PostgreSQL migration connection is invalid',
);
}
const mode = environment.QL3_POSTGRES_TLS_MODE ?? 'verify-full';
if (mode !== 'verify-full' && mode !== 'disable') {
throw new PostgresMigrationProcessConfigError(
'QL3_POSTGRES_TLS_MODE must be verify-full or disable',
);
}
if (
mode === 'disable' &&
!booleanValue(environment, 'QL3_POSTGRES_ALLOW_INSECURE')
) {
throw new PostgresMigrationProcessConfigError(
'disabling PostgreSQL TLS requires QL3_POSTGRES_ALLOW_INSECURE=true',
);
}
const servername = boundedValue(
environment,
'QL3_POSTGRES_TLS_SERVERNAME',
253,
);
if (mode === 'verify-full' && !isPostgresTlsDnsServername(servername)) {
throw new PostgresMigrationProcessConfigError(
'QL3_POSTGRES_TLS_SERVERNAME must be an explicit DNS name for verify-full',
);
}
const certificateAuthorityFile = boundedValue(
environment,
'QL3_POSTGRES_TLS_CA_FILE',
4096,
);
if (mode === 'disable' && certificateAuthorityFile !== undefined) {
throw new PostgresMigrationProcessConfigError(
'QL3_POSTGRES_TLS_CA_FILE cannot be used when TLS is disabled',
);
}
let certificateAuthority: string | undefined;
if (certificateAuthorityFile !== undefined) {
try {
certificateAuthority = loadPostgresCertificateAuthorityFile(
certificateAuthorityFile,
);
} catch {
throw new PostgresMigrationProcessConfigError(
'QL3_POSTGRES_TLS_CA_FILE must contain a bounded trusted CA bundle',
);
}
}
const applicationName =
boundedValue(environment, 'QL3_POSTGRES_APPLICATION_NAME', 63) ??
'qinglong3-cluster-migration';
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,62}$/.test(applicationName)) {
throw new PostgresMigrationProcessConfigError(
'QL3_POSTGRES_APPLICATION_NAME is invalid',
);
}
return Object.freeze({
connection: Object.freeze({
...connection,
tls:
mode === 'disable'
? Object.freeze({ mode: 'disable' as const })
: Object.freeze({
mode: 'verify-full' as const,
...(certificateAuthority === undefined
? {}
: { ca: certificateAuthority }),
servername: servername!,
}),
}),
pool: Object.freeze({
applicationName,
maxConnections: 1,
connectionTimeoutMs: 15_000,
}),
});
}
function processEvent(
event: PostgresMigrationProcessEvent['event'],
): PostgresMigrationProcessEvent {
return Object.freeze({
schemaVersion: 1,
component: 'qinglong3-cluster-migration',
event,
});
}
async function emit(
sink: RunPostgresMigrationProcessOptions['emit'],
value: PostgresMigrationProcessEvent,
): Promise<void> {
if (!sink) return;
try {
await sink(value);
} catch {
// Diagnostics cannot replace migration or close outcomes.
}
}
/** Opens exactly one migration-role Pool, runs the reviewed stream and closes. */
export async function runPostgresMigrationProcess(
options: RunPostgresMigrationProcessOptions,
): Promise<'migrated'> {
if (!options || typeof options !== 'object') {
throw new TypeError('PostgreSQL migration process options are invalid');
}
const config = loadPostgresMigrationProcessConfig(options.environment);
const openDatabase =
options.openDatabase ??
createPostgresDatabaseOpener({
role: 'migration',
connection: config.connection,
pool: config.pool,
onPoolError() {
// Awaited migration statements remain authoritative. An idle Pool
// error will be observed by the migration or final close.
},
});
const migrate = options.migrate ?? runPostgresMigrations;
if (
typeof openDatabase !== 'function' ||
typeof migrate !== 'function' ||
(options.emit !== undefined && typeof options.emit !== 'function')
) {
throw new TypeError('PostgreSQL migration process adapters are invalid');
}
let database: PostgresDatabaseResource | undefined;
let primaryError: unknown;
try {
database = await openDatabase();
await emit(options.emit, processEvent('migration_started'));
await migrate({ pool: database.pool });
await emit(options.emit, processEvent('migration_completed'));
} catch (error) {
primaryError = error;
}
if (database) {
try {
await database.close();
} catch (error) {
primaryError ??= error;
}
}
if (primaryError) throw primaryError;
return 'migrated';
}
@@ -0,0 +1,123 @@
import {
POSTGRESQL_MAIN_MIGRATION_STREAM_ID,
type PostgresMigrationContext,
} from './postgresMigrationStreamStore';
import type { MigrationStreamDefinition } from '@qinglong/runtime-core';
import { pg0001SchemaCapabilityMigration } from './pg-0001-schema-capability';
import { pg0002RunCoreMigration } from './pg-0002-run-core';
import { pg0003RunRetryPolicyMigration } from './pg-0003-run-retry-policy';
import { pg0004ProjectPolicyMigration } from './pg-0004-project-policy';
import { pg0005ApiCredentialSecurityAuditMigration } from './pg-0005-api-credential-security-audit';
import { pg0006IdentityCredentialAdministrationMigration } from './pg-0006-identity-credential-administration';
import { pg0007ClusterRecoveryIndexesMigration } from './pg-0007-cluster-recovery-indexes';
import { pg0008RunRecoveryClaimsMigration } from './pg-0008-run-recovery-claims';
import { pg0009WorkerSessionRunLeaseMigration } from './pg-0009-worker-session-run-lease';
import { pg0010WorkerIngressAttestationMigration } from './pg-0010-worker-ingress-attestation';
import { pg0011ApiCredentialPepperBindingMigration } from './pg-0011-api-credential-pepper-binding';
import { pg0012TaskTriggerDefinitionsMigration } from './pg-0012-task-trigger-definitions';
import { pg0013TaskExecutionRevisionsMigration } from './pg-0013-task-execution-revisions';
import { pg0014ClusterSchedulerAdmissionMigration } from './pg-0014-cluster-scheduler-admission';
import { pg0015WorkerCredentialDeliveryLedgerMigration } from './pg-0015-worker-credential-delivery-ledger';
import { pg0016WorkerCredentialStageDiscardLedgerMigration } from './pg-0016-worker-credential-stage-discard-ledger';
import { pg0017DatabaseRoleGrantsMigration } from './pg-0017-database-role-grants';
import { pg0018PluginPackageInstallsMigration } from './pg-0018-plugin-package-installs';
import { pg0019ApprovedActionsMigration } from './pg-0019-approved-actions';
import { pg0020PluginPackageAdmissionReceiptsMigration } from './pg-0020-plugin-package-admission-receipts';
import { pg0021ApprovedActionExecutionsAndPackageProposalsMigration } from './pg-0021-approved-action-executions-and-package-proposals';
import { pg0022PluginPackageAuthoritySplitMigration } from './pg-0022-plugin-package-authority-split';
import { pg0023PluginPackageManagementQuotaMigration } from './pg-0023-plugin-package-management-quota';
import { pg0024PluginPackageIdentityKeysetLedgerMigration } from './pg-0024-plugin-package-identity-keyset-ledger';
import { pg0025PluginPackageMaterializedRevisionsMigration } from './pg-0025-plugin-package-materialized-revisions';
import { pg0026PluginPackageTaskReconciliationsMigration } from './pg-0026-plugin-package-task-reconciliations';
import { pg0027ProjectToolDefinitionSnapshotsMigration } from './pg-0027-project-tool-definition-snapshots';
import { pg0028StepRunsMigration } from './pg-0028-step-runs';
import { pg0029ToolExecutionEvidenceMigration } from './pg-0029-tool-execution-evidence';
import { pg0030ToolExecutionStartBarriersMigration } from './pg-0030-tool-execution-start-barriers';
import { pg0031ToolInvocationArtifactsMigration } from './pg-0031-tool-invocation-artifacts';
import { pg0032ToolExecutionArtifactBindingsMigration } from './pg-0032-tool-execution-artifact-bindings';
import { pg0033ToolExecutionCompletionsMigration } from './pg-0033-tool-execution-completions';
import { pg0034ToolExecutionFailureCompletionsMigration } from './pg-0034-tool-execution-failure-completions';
import { pg0035ToolResultKeyCatalogMigration } from './pg-0035-tool-result-key-catalog';
import { pg0036ToolResultRekeyOverlaysMigration } from './pg-0036-tool-result-rekey-overlays';
import { pg0037PluginPackageQuarantineMigration } from './pg-0037-plugin-package-quarantine';
import { pg0038PluginPackagePublisherProvenanceMigration } from './pg-0038-plugin-package-publisher-provenance';
import { pg0039PluginPackagePublisherTrustAuthorityMigration } from './pg-0039-plugin-package-publisher-trust-authority';
import { pg0040PluginPackagePublisherTrustTransitionsMigration } from './pg-0040-plugin-package-publisher-trust-transitions';
import { pg0041PluginPackageLifecycleMigration } from './pg-0041-plugin-package-lifecycle';
import { pg0042PluginPackageLifecyclePlansMigration } from './pg-0042-plugin-package-lifecycle-plans';
import { pg0043PluginPackageAutomationPublicationsMigration } from './pg-0043-plugin-package-automation-publications';
import { pg0044PluginPackageAutomationStartGuardMigration } from './pg-0044-plugin-package-automation-start-guard';
import { pg0045PluginPackageWorkflowAdmissionsMigration } from './pg-0045-plugin-package-workflow-admissions';
import { pg0046PluginPackageWorkflowTaskAttemptAdmissionsMigration } from './pg-0046-plugin-package-workflow-task-attempt-admissions';
import { pg0047WorkerCredentialManagementPlansMigration } from './pg-0047-worker-credential-management-plans';
import { pg0048WorkerCredentialPreapprovedActivationMigration } from './pg-0048-worker-credential-preapproved-activation';
import { pg0049WorkerCredentialExecutionReceiptsMigration } from './pg-0049-worker-credential-execution-receipts';
import { pg0050WorkerCredentialManagementBoundaryMigration } from './pg-0050-worker-credential-management-boundary';
import { pg0051AutomationManagementBoundaryMigration } from './pg-0051-automation-management-boundary';
import { pg0052AutomationManagementIdentityKeysetLedgerMigration } from './pg-0052-automation-management-identity-keyset-ledger';
import { pg0053PluginPackageWorkflowRunListIndexMigration } from './pg-0053-plugin-package-workflow-run-list-index';
import { pg0054ApprovalManagementBoundaryMigration } from './pg-0054-approval-management-boundary';
export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMigrationContext> =
Object.freeze({
id: POSTGRESQL_MAIN_MIGRATION_STREAM_ID,
dialect: 'postgresql',
migrationIdScheme: 'postgres-prefixed',
checksumScheme: 'sha256',
migrations: Object.freeze([
pg0001SchemaCapabilityMigration,
pg0002RunCoreMigration,
pg0003RunRetryPolicyMigration,
pg0004ProjectPolicyMigration,
pg0005ApiCredentialSecurityAuditMigration,
pg0006IdentityCredentialAdministrationMigration,
pg0007ClusterRecoveryIndexesMigration,
pg0008RunRecoveryClaimsMigration,
pg0009WorkerSessionRunLeaseMigration,
pg0010WorkerIngressAttestationMigration,
pg0011ApiCredentialPepperBindingMigration,
pg0012TaskTriggerDefinitionsMigration,
pg0013TaskExecutionRevisionsMigration,
pg0014ClusterSchedulerAdmissionMigration,
pg0015WorkerCredentialDeliveryLedgerMigration,
pg0016WorkerCredentialStageDiscardLedgerMigration,
pg0017DatabaseRoleGrantsMigration,
pg0018PluginPackageInstallsMigration,
pg0019ApprovedActionsMigration,
pg0020PluginPackageAdmissionReceiptsMigration,
pg0021ApprovedActionExecutionsAndPackageProposalsMigration,
pg0022PluginPackageAuthoritySplitMigration,
pg0023PluginPackageManagementQuotaMigration,
pg0024PluginPackageIdentityKeysetLedgerMigration,
pg0025PluginPackageMaterializedRevisionsMigration,
pg0026PluginPackageTaskReconciliationsMigration,
pg0027ProjectToolDefinitionSnapshotsMigration,
pg0028StepRunsMigration,
pg0029ToolExecutionEvidenceMigration,
pg0030ToolExecutionStartBarriersMigration,
pg0031ToolInvocationArtifactsMigration,
pg0032ToolExecutionArtifactBindingsMigration,
pg0033ToolExecutionCompletionsMigration,
pg0034ToolExecutionFailureCompletionsMigration,
pg0035ToolResultKeyCatalogMigration,
pg0036ToolResultRekeyOverlaysMigration,
pg0037PluginPackageQuarantineMigration,
pg0038PluginPackagePublisherProvenanceMigration,
pg0039PluginPackagePublisherTrustAuthorityMigration,
pg0040PluginPackagePublisherTrustTransitionsMigration,
pg0041PluginPackageLifecycleMigration,
pg0042PluginPackageLifecyclePlansMigration,
pg0043PluginPackageAutomationPublicationsMigration,
pg0044PluginPackageAutomationStartGuardMigration,
pg0045PluginPackageWorkflowAdmissionsMigration,
pg0046PluginPackageWorkflowTaskAttemptAdmissionsMigration,
pg0047WorkerCredentialManagementPlansMigration,
pg0048WorkerCredentialPreapprovedActivationMigration,
pg0049WorkerCredentialExecutionReceiptsMigration,
pg0050WorkerCredentialManagementBoundaryMigration,
pg0051AutomationManagementBoundaryMigration,
pg0052AutomationManagementIdentityKeysetLedgerMigration,
pg0053PluginPackageWorkflowRunListIndexMigration,
pg0054ApprovalManagementBoundaryMigration,
]),
});
@@ -0,0 +1,28 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const POSTGRESQL_SCHEMA_CAPABILITY_TABLE = 'schema_capabilities';
export const pg0001SchemaCapabilityMigration = definePostgresSqlMigration({
id: 'pg-0001-schema-capability',
statements: [
`
CREATE TABLE "ql3"."${POSTGRESQL_SCHEMA_CAPABILITY_TABLE}" (
contract_name varchar(64) PRIMARY KEY,
contract_version integer NOT NULL
CONSTRAINT ql3_schema_capabilities_version_check
CHECK (contract_version >= 0),
migration_id varchar(128) NOT NULL,
capabilities jsonb NOT NULL
CONSTRAINT ql3_schema_capabilities_payload_check
CHECK (jsonb_typeof(capabilities) = 'object'),
updated_at_ms bigint NOT NULL
CONSTRAINT ql3_schema_capabilities_updated_at_check
CHECK (updated_at_ms >= 0),
CONSTRAINT ql3_schema_capabilities_migration_fk
FOREIGN KEY (migration_id)
REFERENCES "ql3"."schema_migrations" (migration_id)
DEFERRABLE INITIALLY DEFERRED
)
`.trim(),
],
});
@@ -0,0 +1,224 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const POSTGRESQL_RUN_TABLE = 'runs';
export const POSTGRESQL_RUN_ATTEMPT_TABLE = 'run_attempts';
export const POSTGRESQL_RUN_EVENT_TABLE = 'run_events';
export const pg0002RunCoreMigration = definePostgresSqlMigration({
id: 'pg-0002-run-core',
statements: [
`
CREATE TABLE "ql3"."${POSTGRESQL_RUN_TABLE}" (
id varchar(36) PRIMARY KEY,
project_id varchar(128) NOT NULL,
task_id varchar(255) NOT NULL,
task_revision varchar(128) NOT NULL,
task_name varchar(255),
task_snapshot_ref varchar(512),
parent_run_id varchar(36),
retry_of_run_id varchar(36),
trigger_id varchar(36),
trigger_type varchar(64) NOT NULL,
execution_origin varchar(64) NOT NULL,
execution_owner varchar(16) NOT NULL
CONSTRAINT ql3_runs_execution_owner_check
CHECK (execution_owner = 'runtime'),
triggered_by varchar(255),
request_id varchar(128),
scheduled_for_ms bigint
CONSTRAINT ql3_runs_scheduled_for_check
CHECK (scheduled_for_ms IS NULL OR scheduled_for_ms >= 0),
status varchar(32) NOT NULL
CONSTRAINT ql3_runs_status_check
CHECK (status IN (
'created', 'queued', 'dispatching', 'running', 'waiting_approval',
'retry_wait', 'lost', 'succeeded', 'failed', 'cancelled', 'timed_out'
)),
version integer NOT NULL DEFAULT 0
CONSTRAINT ql3_runs_version_check
CHECK (version >= 0),
event_sequence integer NOT NULL DEFAULT 0
CONSTRAINT ql3_runs_event_sequence_check
CHECK (event_sequence >= 0),
priority integer NOT NULL DEFAULT 0,
idempotency_key varchar(255),
input_ref varchar(512),
output_ref varchar(512),
created_at_ms bigint NOT NULL
CONSTRAINT ql3_runs_created_at_check
CHECK (created_at_ms >= 0),
queued_at_ms bigint
CONSTRAINT ql3_runs_queued_at_check
CHECK (queued_at_ms IS NULL OR queued_at_ms >= 0),
started_at_ms bigint
CONSTRAINT ql3_runs_started_at_check
CHECK (started_at_ms IS NULL OR started_at_ms >= 0),
finished_at_ms bigint
CONSTRAINT ql3_runs_finished_at_check
CHECK (finished_at_ms IS NULL OR finished_at_ms >= 0),
cancel_requested_at_ms bigint
CONSTRAINT ql3_runs_cancel_requested_at_check
CHECK (cancel_requested_at_ms IS NULL OR cancel_requested_at_ms >= 0),
cancel_reason varchar(16)
CONSTRAINT ql3_runs_cancel_reason_check
CHECK (cancel_reason IS NULL OR cancel_reason IN (
'user', 'policy', 'shutdown', 'reconcile', 'timeout'
)),
error_code varchar(128),
error_summary varchar(1024),
CONSTRAINT ql3_runs_parent_fk
FOREIGN KEY (parent_run_id) REFERENCES "ql3"."${POSTGRESQL_RUN_TABLE}" (id),
CONSTRAINT ql3_runs_retry_of_fk
FOREIGN KEY (retry_of_run_id) REFERENCES "ql3"."${POSTGRESQL_RUN_TABLE}" (id)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_runs_project_idempotency_uidx
ON "ql3"."${POSTGRESQL_RUN_TABLE}" (project_id, idempotency_key)
WHERE idempotency_key IS NOT NULL
`.trim(),
`
CREATE INDEX ql3_runs_project_created_idx
ON "ql3"."${POSTGRESQL_RUN_TABLE}" (project_id, created_at_ms, id)
`.trim(),
`
CREATE INDEX ql3_runs_task_created_idx
ON "ql3"."${POSTGRESQL_RUN_TABLE}" (task_id, created_at_ms, id)
`.trim(),
`
CREATE INDEX ql3_runs_dispatch_candidates_idx
ON "ql3"."${POSTGRESQL_RUN_TABLE}" (priority DESC, queued_at_ms, id)
WHERE execution_owner = 'runtime'
AND status IN ('queued', 'dispatching')
AND cancel_requested_at_ms IS NULL
AND queued_at_ms IS NOT NULL
`.trim(),
`
CREATE TABLE "ql3"."${POSTGRESQL_RUN_ATTEMPT_TABLE}" (
id varchar(36) PRIMARY KEY,
run_id varchar(36) NOT NULL,
step_run_id varchar(36),
attempt integer NOT NULL
CONSTRAINT ql3_run_attempts_attempt_check
CHECK (attempt >= 1),
status varchar(32) NOT NULL
CONSTRAINT ql3_run_attempts_status_check
CHECK (status IN (
'claimed', 'starting', 'running', 'succeeded', 'failed',
'cancelled', 'timed_out', 'lost'
)),
executor_type varchar(64) NOT NULL,
worker_id varchar(128),
executor_handle varchar(2048),
pid integer
CONSTRAINT ql3_run_attempts_pid_check
CHECK (pid IS NULL OR pid >= 1),
log_artifact_id varchar(36),
lease_token varchar(128),
lease_expires_at_ms bigint
CONSTRAINT ql3_run_attempts_lease_expiry_check
CHECK (lease_expires_at_ms IS NULL OR lease_expires_at_ms >= 0),
deadline_at_ms bigint
CONSTRAINT ql3_run_attempts_deadline_check
CHECK (deadline_at_ms IS NULL OR deadline_at_ms >= 0),
callback_token_hash varchar(128),
callback_sequence integer NOT NULL DEFAULT 0
CONSTRAINT ql3_run_attempts_callback_sequence_check
CHECK (callback_sequence >= 0),
created_at_ms bigint NOT NULL
CONSTRAINT ql3_run_attempts_created_at_check
CHECK (created_at_ms >= 0),
started_at_ms bigint
CONSTRAINT ql3_run_attempts_started_at_check
CHECK (started_at_ms IS NULL OR started_at_ms >= 0),
finished_at_ms bigint
CONSTRAINT ql3_run_attempts_finished_at_check
CHECK (finished_at_ms IS NULL OR finished_at_ms >= 0),
exit_code integer,
error_code varchar(128),
error_summary varchar(1024),
CONSTRAINT ql3_run_attempts_run_fk
FOREIGN KEY (run_id) REFERENCES "ql3"."${POSTGRESQL_RUN_TABLE}" (id)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_run_attempts_run_attempt_uidx
ON "ql3"."${POSTGRESQL_RUN_ATTEMPT_TABLE}" (run_id, attempt)
`.trim(),
`
CREATE INDEX ql3_run_attempts_dispatch_candidates_idx
ON "ql3"."${POSTGRESQL_RUN_ATTEMPT_TABLE}" (status, run_id, created_at_ms, id)
`.trim(),
`
CREATE INDEX ql3_run_attempts_lease_idx
ON "ql3"."${POSTGRESQL_RUN_ATTEMPT_TABLE}" (lease_expires_at_ms, id)
WHERE lease_expires_at_ms IS NOT NULL
`.trim(),
`
CREATE TABLE "ql3"."${POSTGRESQL_RUN_EVENT_TABLE}" (
id varchar(36) PRIMARY KEY,
run_id varchar(36) NOT NULL,
sequence integer NOT NULL
CONSTRAINT ql3_run_events_sequence_check
CHECK (sequence >= 1),
type varchar(128) NOT NULL,
dedupe_key varchar(255),
actor_type varchar(64) NOT NULL
CONSTRAINT ql3_run_events_actor_type_check
CHECK (actor_type IN (
'user', 'api_app', 'trigger', 'agent', 'mcp_client', 'worker',
'executor', 'system', 'legacy_shell', 'scheduler', 'reconciler',
'compatibility'
)),
actor_id varchar(255),
attempt_id varchar(36),
step_run_id varchar(36),
payload jsonb NOT NULL
CONSTRAINT ql3_run_events_payload_check
CHECK (jsonb_typeof(payload) = 'object'),
created_at_ms bigint NOT NULL
CONSTRAINT ql3_run_events_created_at_check
CHECK (created_at_ms >= 0),
CONSTRAINT ql3_run_events_run_fk
FOREIGN KEY (run_id) REFERENCES "ql3"."${POSTGRESQL_RUN_TABLE}" (id),
CONSTRAINT ql3_run_events_attempt_fk
FOREIGN KEY (attempt_id) REFERENCES "ql3"."${POSTGRESQL_RUN_ATTEMPT_TABLE}" (id)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_run_events_run_sequence_uidx
ON "ql3"."${POSTGRESQL_RUN_EVENT_TABLE}" (run_id, sequence)
`.trim(),
`
CREATE UNIQUE INDEX ql3_run_events_run_dedupe_uidx
ON "ql3"."${POSTGRESQL_RUN_EVENT_TABLE}" (run_id, dedupe_key)
WHERE dedupe_key IS NOT NULL
`.trim(),
`
CREATE INDEX ql3_run_events_run_created_idx
ON "ql3"."${POSTGRESQL_RUN_EVENT_TABLE}" (run_id, created_at_ms, id)
`.trim(),
`
INSERT INTO "ql3"."schema_capabilities" (
contract_name,
contract_version,
migration_id,
capabilities,
updated_at_ms
)
VALUES (
'control-core',
1,
'pg-0002-run-core',
'{"run_core":1}'::jsonb,
floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint
)
ON CONFLICT (contract_name) DO UPDATE
SET contract_version = EXCLUDED.contract_version,
migration_id = EXCLUDED.migration_id,
capabilities = EXCLUDED.capabilities,
updated_at_ms = EXCLUDED.updated_at_ms
WHERE "schema_capabilities".contract_version < EXCLUDED.contract_version
`.trim(),
],
});
@@ -0,0 +1,78 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const POSTGRESQL_RUN_RETRY_POLICY_TABLE = 'run_retry_policies';
export const pg0003RunRetryPolicyMigration = definePostgresSqlMigration({
id: 'pg-0003-run-retry-policy',
statements: [
`
ALTER TABLE "ql3"."runs"
ADD COLUMN legacy_cron_id integer
CONSTRAINT ql3_runs_legacy_cron_id_check
CHECK (legacy_cron_id IS NULL OR legacy_cron_id >= 1)
`.trim(),
`
CREATE TABLE "ql3"."${POSTGRESQL_RUN_RETRY_POLICY_TABLE}" (
run_id varchar(36) PRIMARY KEY,
max_attempts integer NOT NULL
CONSTRAINT ql3_run_retry_policies_max_attempts_check
CHECK (max_attempts BETWEEN 1 AND 16),
retry_on_lost boolean NOT NULL,
safety varchar(16) NOT NULL
CONSTRAINT ql3_run_retry_policies_safety_check
CHECK (safety IN ('unknown', 'idempotent', 'deduplicated')),
backoff_base_ms bigint NOT NULL
CONSTRAINT ql3_run_retry_policies_backoff_base_check
CHECK (backoff_base_ms BETWEEN 0 AND 86400000),
backoff_max_ms bigint NOT NULL
CONSTRAINT ql3_run_retry_policies_backoff_max_check
CHECK (backoff_max_ms BETWEEN backoff_base_ms AND 86400000),
next_attempt_at_ms bigint
CONSTRAINT ql3_run_retry_policies_next_attempt_check
CHECK (next_attempt_at_ms IS NULL OR next_attempt_at_ms >= 0),
version integer NOT NULL DEFAULT 0
CONSTRAINT ql3_run_retry_policies_version_check
CHECK (version >= 0),
created_at_ms bigint NOT NULL
CONSTRAINT ql3_run_retry_policies_created_at_check
CHECK (created_at_ms >= 0),
updated_at_ms bigint NOT NULL
CONSTRAINT ql3_run_retry_policies_updated_at_check
CHECK (updated_at_ms >= created_at_ms),
CONSTRAINT ql3_run_retry_policies_run_fk
FOREIGN KEY (run_id) REFERENCES "ql3"."runs" (id) ON DELETE CASCADE
)
`.trim(),
`
CREATE INDEX ql3_run_retry_policies_due_idx
ON "ql3"."${POSTGRESQL_RUN_RETRY_POLICY_TABLE}" (next_attempt_at_ms, run_id)
WHERE next_attempt_at_ms IS NOT NULL
`.trim(),
`
CREATE INDEX ql3_runs_lost_retry_idx
ON "ql3"."runs" (execution_owner, status, id)
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 2,
migration_id = 'pg-0003-run-retry-policy',
capabilities = '{"run_core":1,"run_retry_policy":1}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 1
AND migration_id = 'pg-0002-run-core'
AND capabilities = '{"run_core":1}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 1'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,119 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const POSTGRESQL_PROJECT_TABLE = 'projects';
export const POSTGRESQL_PROJECT_ROLE_BINDING_TABLE = 'project_role_bindings';
export const pg0004ProjectPolicyMigration = definePostgresSqlMigration({
id: 'pg-0004-project-policy',
statements: [
`
CREATE TABLE "ql3"."${POSTGRESQL_PROJECT_TABLE}" (
id varchar(128) PRIMARY KEY
CONSTRAINT ql3_projects_id_check CHECK (char_length(id) >= 1),
name varchar(255) NOT NULL
CONSTRAINT ql3_projects_name_check CHECK (char_length(name) >= 1),
slug varchar(128) NOT NULL
CONSTRAINT ql3_projects_slug_check
CHECK (slug ~ '^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$'),
status varchar(16) NOT NULL
CONSTRAINT ql3_projects_status_check
CHECK (status IN ('active', 'archived')),
version integer NOT NULL
CONSTRAINT ql3_projects_version_check
CHECK (version BETWEEN 1 AND 2147483647),
created_at_ms bigint NOT NULL
CONSTRAINT ql3_projects_created_at_check CHECK (created_at_ms >= 0),
updated_at_ms bigint NOT NULL
CONSTRAINT ql3_projects_updated_at_check
CHECK (updated_at_ms >= created_at_ms)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_projects_slug_uidx
ON "ql3"."${POSTGRESQL_PROJECT_TABLE}" (slug)
`.trim(),
`
INSERT INTO "ql3"."${POSTGRESQL_PROJECT_TABLE}" (
id, name, slug, status, version, created_at_ms, updated_at_ms
)
VALUES ('default', 'Default', 'default', 'active', 1, 0, 0)
`.trim(),
`
CREATE TABLE "ql3"."${POSTGRESQL_PROJECT_ROLE_BINDING_TABLE}" (
project_id varchar(128) NOT NULL,
subject_type varchar(32) NOT NULL
CONSTRAINT ql3_project_role_bindings_subject_type_check
CHECK (subject_type IN ('user', 'api_app', 'mcp_client', 'agent', 'system', 'worker')),
subject_id varchar(255) NOT NULL
CONSTRAINT ql3_project_role_bindings_subject_id_check
CHECK (char_length(subject_id) >= 1),
version integer NOT NULL
CONSTRAINT ql3_project_role_bindings_version_check
CHECK (version BETWEEN 1 AND 2147483647),
state varchar(16) NOT NULL
CONSTRAINT ql3_project_role_bindings_state_check
CHECK (state IN ('active', 'revoked')),
role varchar(16),
mutation_id varchar(64) NOT NULL
CONSTRAINT ql3_project_role_bindings_mutation_id_check
CHECK (mutation_id ~ '^[A-Za-z0-9._:-]{1,64}$'),
changed_by_type varchar(32) NOT NULL
CONSTRAINT ql3_project_role_bindings_changed_by_type_check
CHECK (changed_by_type IN ('user', 'api_app', 'mcp_client', 'agent', 'system', 'worker')),
changed_by_id varchar(255) NOT NULL
CONSTRAINT ql3_project_role_bindings_changed_by_id_check
CHECK (char_length(changed_by_id) >= 1),
created_at_ms bigint NOT NULL
CONSTRAINT ql3_project_role_bindings_created_at_check
CHECK (created_at_ms >= 0),
CONSTRAINT project_role_bindings_pkey
PRIMARY KEY (project_id, subject_type, subject_id, version),
CONSTRAINT ql3_project_role_bindings_role_state_check
CHECK (
(state = 'active' AND role IN ('owner', 'admin', 'operator', 'viewer'))
OR (state = 'revoked' AND role IS NULL)
),
CONSTRAINT ql3_project_role_bindings_project_fk
FOREIGN KEY (project_id) REFERENCES "ql3"."${POSTGRESQL_PROJECT_TABLE}" (id)
ON DELETE CASCADE
)
`.trim(),
`
CREATE INDEX ql3_project_role_bindings_current_idx
ON "ql3"."${POSTGRESQL_PROJECT_ROLE_BINDING_TABLE}"
(project_id, subject_type, subject_id, version DESC)
`.trim(),
`
CREATE UNIQUE INDEX ql3_project_role_bindings_mutation_uidx
ON "ql3"."${POSTGRESQL_PROJECT_ROLE_BINDING_TABLE}"
(project_id, mutation_id)
`.trim(),
`
CREATE INDEX ql3_project_role_bindings_subject_idx
ON "ql3"."${POSTGRESQL_PROJECT_ROLE_BINDING_TABLE}"
(subject_type, subject_id, project_id, version DESC)
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 3,
migration_id = 'pg-0004-project-policy',
capabilities = '{"project_policy":1,"run_core":1,"run_retry_policy":1}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 2
AND migration_id = 'pg-0003-run-retry-policy'
AND capabilities = '{"run_core":1,"run_retry_policy":1}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 2'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,205 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const POSTGRESQL_IDENTITY_SUBJECT_TABLE = 'identity_subjects';
export const POSTGRESQL_API_CREDENTIAL_TABLE = 'api_credentials';
export const POSTGRESQL_SECURITY_AUDIT_EVENT_TABLE = 'security_audit_events';
export const pg0005ApiCredentialSecurityAuditMigration =
definePostgresSqlMigration({
id: 'pg-0005-api-credential-security-audit',
statements: [
`
CREATE TABLE "ql3"."${POSTGRESQL_IDENTITY_SUBJECT_TABLE}" (
subject_type varchar(32) NOT NULL
CONSTRAINT ql3_identity_subjects_type_check
CHECK (subject_type IN ('user', 'api_app', 'mcp_client', 'agent', 'system', 'worker')),
subject_id varchar(255) NOT NULL
CONSTRAINT ql3_identity_subjects_id_check
CHECK (char_length(subject_id) >= 1),
status varchar(16) NOT NULL
CONSTRAINT ql3_identity_subjects_status_check
CHECK (status IN ('active', 'disabled')),
version integer NOT NULL
CONSTRAINT ql3_identity_subjects_version_check
CHECK (version BETWEEN 1 AND 2147483647),
created_at_ms bigint NOT NULL
CONSTRAINT ql3_identity_subjects_created_at_check
CHECK (created_at_ms >= 0),
updated_at_ms bigint NOT NULL
CONSTRAINT ql3_identity_subjects_updated_at_check
CHECK (updated_at_ms >= created_at_ms),
CONSTRAINT identity_subjects_pkey
PRIMARY KEY (subject_type, subject_id)
)
`.trim(),
`
CREATE INDEX ql3_identity_subjects_status_idx
ON "ql3"."${POSTGRESQL_IDENTITY_SUBJECT_TABLE}"
(status, subject_type, subject_id)
`.trim(),
`
CREATE TABLE "ql3"."${POSTGRESQL_API_CREDENTIAL_TABLE}" (
credential_id varchar(64) NOT NULL
CONSTRAINT ql3_api_credentials_id_check
CHECK (credential_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$'),
version integer NOT NULL
CONSTRAINT ql3_api_credentials_version_check
CHECK (version BETWEEN 1 AND 2147483647),
state varchar(16) NOT NULL
CONSTRAINT ql3_api_credentials_state_check
CHECK (state IN ('active', 'revoked')),
subject_type varchar(32) NOT NULL
CONSTRAINT ql3_api_credentials_subject_type_check
CHECK (subject_type IN ('user', 'api_app', 'mcp_client', 'agent')),
subject_id varchar(255) NOT NULL
CONSTRAINT ql3_api_credentials_subject_id_check
CHECK (char_length(subject_id) >= 1),
secret_digest char(64) NOT NULL
CONSTRAINT ql3_api_credentials_secret_digest_check
CHECK (secret_digest ~ '^[a-f0-9]{64}$'),
created_at_ms bigint NOT NULL
CONSTRAINT ql3_api_credentials_created_at_check
CHECK (created_at_ms >= 0),
not_before_at_ms bigint NOT NULL
CONSTRAINT ql3_api_credentials_not_before_check
CHECK (not_before_at_ms >= created_at_ms),
expires_at_ms bigint NOT NULL
CONSTRAINT ql3_api_credentials_expires_at_check
CHECK (expires_at_ms > not_before_at_ms),
CONSTRAINT api_credentials_pkey
PRIMARY KEY (credential_id, version),
CONSTRAINT ql3_api_credentials_subject_fk
FOREIGN KEY (subject_type, subject_id)
REFERENCES "ql3"."${POSTGRESQL_IDENTITY_SUBJECT_TABLE}"
(subject_type, subject_id)
ON DELETE RESTRICT
)
`.trim(),
`
CREATE INDEX ql3_api_credentials_current_idx
ON "ql3"."${POSTGRESQL_API_CREDENTIAL_TABLE}"
(credential_id, version DESC)
`.trim(),
`
CREATE INDEX ql3_api_credentials_subject_idx
ON "ql3"."${POSTGRESQL_API_CREDENTIAL_TABLE}"
(subject_type, subject_id, credential_id, version DESC)
`.trim(),
`
CREATE TABLE "ql3"."${POSTGRESQL_SECURITY_AUDIT_EVENT_TABLE}" (
event_id uuid PRIMARY KEY,
request_id varchar(128) NOT NULL
CONSTRAINT ql3_security_audit_events_request_id_check
CHECK (request_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'),
operation_id varchar(128) NOT NULL
CONSTRAINT ql3_security_audit_events_operation_id_check
CHECK (operation_id ~ '^[a-z][a-z0-9_.:-]{0,127}$'),
project_id varchar(128)
CONSTRAINT ql3_security_audit_events_project_id_check
CHECK (
project_id IS NULL
OR project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
),
subject_type varchar(32)
CONSTRAINT ql3_security_audit_events_subject_type_check
CHECK (
subject_type IS NULL
OR subject_type IN ('user', 'api_app', 'mcp_client', 'agent', 'system', 'worker')
),
subject_id varchar(255)
CONSTRAINT ql3_security_audit_events_subject_id_check
CHECK (subject_id IS NULL OR char_length(subject_id) >= 1),
authentication_id varchar(128)
CONSTRAINT ql3_security_audit_events_authentication_id_check
CHECK (
authentication_id IS NULL
OR authentication_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
),
outcome varchar(32) NOT NULL
CONSTRAINT ql3_security_audit_events_outcome_check
CHECK (
outcome IN (
'authentication_rejected',
'authentication_unavailable',
'authorization_unavailable',
'denied',
'approval_required',
'allowed'
)
),
reasons jsonb NOT NULL
CONSTRAINT ql3_security_audit_events_reasons_check
CHECK (
jsonb_typeof(reasons) = 'array'
AND jsonb_array_length(reasons) BETWEEN 1 AND 8
AND octet_length(reasons::text) <= 1024
),
project_version integer
CONSTRAINT ql3_security_audit_events_project_version_check
CHECK (project_version IS NULL OR project_version >= 1),
binding_version integer
CONSTRAINT ql3_security_audit_events_binding_version_check
CHECK (binding_version IS NULL OR binding_version >= 1),
occurred_at_ms bigint NOT NULL
CONSTRAINT ql3_security_audit_events_occurred_at_check
CHECK (occurred_at_ms >= 0),
CONSTRAINT ql3_security_audit_events_identity_check
CHECK (
(
outcome IN ('authentication_rejected', 'authentication_unavailable')
AND subject_type IS NULL
AND subject_id IS NULL
AND authentication_id IS NULL
)
OR (
outcome NOT IN ('authentication_rejected', 'authentication_unavailable')
AND subject_type IS NOT NULL
AND subject_id IS NOT NULL
AND authentication_id IS NOT NULL
)
),
CONSTRAINT ql3_security_audit_events_fence_check
CHECK (project_version IS NOT NULL OR binding_version IS NULL)
)
`.trim(),
`
CREATE INDEX ql3_security_audit_events_occurred_idx
ON "ql3"."${POSTGRESQL_SECURITY_AUDIT_EVENT_TABLE}"
(occurred_at_ms DESC, event_id)
`.trim(),
`
CREATE INDEX ql3_security_audit_events_subject_idx
ON "ql3"."${POSTGRESQL_SECURITY_AUDIT_EVENT_TABLE}"
(subject_type, subject_id, occurred_at_ms DESC, event_id)
WHERE subject_type IS NOT NULL
`.trim(),
`
CREATE INDEX ql3_security_audit_events_project_idx
ON "ql3"."${POSTGRESQL_SECURITY_AUDIT_EVENT_TABLE}"
(project_id, occurred_at_ms DESC, event_id)
WHERE project_id IS NOT NULL
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 4,
migration_id = 'pg-0005-api-credential-security-audit',
capabilities = '{"api_credential":1,"project_policy":1,"run_core":1,"run_retry_policy":1,"security_audit":1}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 3
AND migration_id = 'pg-0004-project-policy'
AND capabilities = '{"project_policy":1,"run_core":1,"run_retry_policy":1}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 3'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,316 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const POSTGRESQL_IDENTITY_SUBJECT_MUTATION_TABLE =
'identity_subject_mutations';
export const POSTGRESQL_API_CREDENTIAL_MUTATION_TABLE =
'api_credential_mutations';
export const pg0006IdentityCredentialAdministrationMigration =
definePostgresSqlMigration({
id: 'pg-0006-identity-credential-administration',
statements: [
`
CREATE TABLE "ql3"."${POSTGRESQL_IDENTITY_SUBJECT_MUTATION_TABLE}" (
mutation_id uuid PRIMARY KEY,
operation varchar(16) NOT NULL
CONSTRAINT ql3_identity_subject_mutations_operation_check
CHECK (operation IN ('import', 'register', 'enable', 'disable')),
subject_type varchar(32) NOT NULL
CONSTRAINT ql3_identity_subject_mutations_subject_type_check
CHECK (subject_type IN ('user', 'api_app', 'mcp_client', 'agent', 'system', 'worker')),
subject_id varchar(255) NOT NULL
CONSTRAINT ql3_identity_subject_mutations_subject_id_check
CHECK (char_length(subject_id) >= 1),
subject_version integer NOT NULL
CONSTRAINT ql3_identity_subject_mutations_version_check
CHECK (subject_version BETWEEN 1 AND 2147483647),
expected_previous_version integer NOT NULL
CONSTRAINT ql3_identity_subject_mutations_previous_version_check
CHECK (expected_previous_version BETWEEN 0 AND 2147483646),
status varchar(16) NOT NULL
CONSTRAINT ql3_identity_subject_mutations_status_check
CHECK (status IN ('active', 'disabled')),
changed_by_type varchar(32) NOT NULL
CONSTRAINT ql3_identity_subject_mutations_changed_by_type_check
CHECK (changed_by_type IN ('user', 'system')),
changed_by_id varchar(255) NOT NULL
CONSTRAINT ql3_identity_subject_mutations_changed_by_id_check
CHECK (char_length(changed_by_id) >= 1),
audit_event_id uuid NOT NULL,
identity_created_at_ms bigint NOT NULL
CONSTRAINT ql3_identity_subject_mutations_identity_created_at_check
CHECK (identity_created_at_ms >= 0),
created_at_ms bigint NOT NULL
CONSTRAINT ql3_identity_subject_mutations_created_at_check
CHECK (created_at_ms >= 0),
CONSTRAINT ql3_identity_subject_mutations_version_fence_check
CHECK (subject_version = expected_previous_version + 1),
CONSTRAINT ql3_identity_subject_mutations_transition_check
CHECK (
operation = 'import'
OR (operation = 'register' AND expected_previous_version = 0 AND status = 'active')
OR (operation = 'enable' AND expected_previous_version >= 1 AND status = 'active')
OR (operation = 'disable' AND expected_previous_version >= 1 AND status = 'disabled')
),
CONSTRAINT ql3_identity_subject_mutations_audit_identity_check
CHECK (mutation_id = audit_event_id),
CONSTRAINT ql3_identity_subject_mutations_subject_fk
FOREIGN KEY (subject_type, subject_id)
REFERENCES "ql3"."identity_subjects" (subject_type, subject_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_identity_subject_mutations_audit_fk
FOREIGN KEY (audit_event_id)
REFERENCES "ql3"."security_audit_events" (event_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_identity_subject_mutations_subject_version_uidx
UNIQUE (subject_type, subject_id, subject_version)
)
`.trim(),
`
CREATE INDEX ql3_identity_subject_mutations_actor_idx
ON "ql3"."${POSTGRESQL_IDENTITY_SUBJECT_MUTATION_TABLE}"
(changed_by_type, changed_by_id, created_at_ms DESC, mutation_id)
`.trim(),
`
WITH source AS (
SELECT
subject.*,
md5('ql3-identity-import:' || subject.subject_type || ':' || subject.subject_id) AS digest
FROM "ql3"."identity_subjects" AS subject
), imported AS (
SELECT
source.*,
(
substr(digest, 1, 8) || '-' || substr(digest, 9, 4) || '-4' ||
substr(digest, 14, 3) || '-8' || substr(digest, 18, 3) || '-' ||
substr(digest, 21, 12)
)::uuid AS mutation_id
FROM source
)
INSERT INTO "ql3"."security_audit_events" (
event_id, request_id, operation_id, project_id, subject_type, subject_id,
authentication_id, outcome, reasons, project_version, binding_version,
occurred_at_ms
)
SELECT
mutation_id,
'migration:' || mutation_id::text,
'identity.import',
NULL,
'system',
'pg-0006',
'migration:pg-0006',
'allowed',
'["identity_admin"]'::jsonb,
NULL,
NULL,
created_at_ms
FROM imported
`.trim(),
`
WITH source AS (
SELECT
subject.*,
md5('ql3-identity-import:' || subject.subject_type || ':' || subject.subject_id) AS digest
FROM "ql3"."identity_subjects" AS subject
), imported AS (
SELECT
source.*,
(
substr(digest, 1, 8) || '-' || substr(digest, 9, 4) || '-4' ||
substr(digest, 14, 3) || '-8' || substr(digest, 18, 3) || '-' ||
substr(digest, 21, 12)
)::uuid AS mutation_id
FROM source
)
INSERT INTO "ql3"."${POSTGRESQL_IDENTITY_SUBJECT_MUTATION_TABLE}" (
mutation_id, operation, subject_type, subject_id, subject_version,
expected_previous_version, status, changed_by_type, changed_by_id,
audit_event_id, identity_created_at_ms, created_at_ms
)
SELECT
mutation_id,
'import',
subject_type,
subject_id,
version,
version - 1,
status,
'system',
'pg-0006',
mutation_id,
created_at_ms,
created_at_ms
FROM imported
`.trim(),
`
CREATE TABLE "ql3"."${POSTGRESQL_API_CREDENTIAL_MUTATION_TABLE}" (
mutation_id uuid PRIMARY KEY,
operation varchar(16) NOT NULL
CONSTRAINT ql3_api_credential_mutations_operation_check
CHECK (operation IN ('import', 'issue', 'rotate', 'revoke')),
credential_id varchar(64) NOT NULL
CONSTRAINT ql3_api_credential_mutations_id_check
CHECK (credential_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$'),
credential_version integer NOT NULL
CONSTRAINT ql3_api_credential_mutations_version_check
CHECK (credential_version BETWEEN 1 AND 2147483647),
expected_previous_version integer NOT NULL
CONSTRAINT ql3_api_credential_mutations_previous_version_check
CHECK (expected_previous_version BETWEEN 0 AND 2147483646),
state varchar(16) NOT NULL
CONSTRAINT ql3_api_credential_mutations_state_check
CHECK (state IN ('active', 'revoked')),
subject_type varchar(32) NOT NULL
CONSTRAINT ql3_api_credential_mutations_subject_type_check
CHECK (subject_type IN ('user', 'api_app', 'mcp_client', 'agent')),
subject_id varchar(255) NOT NULL
CONSTRAINT ql3_api_credential_mutations_subject_id_check
CHECK (char_length(subject_id) >= 1),
subject_status varchar(16) NOT NULL
CONSTRAINT ql3_api_credential_mutations_subject_status_check
CHECK (subject_status IN ('active', 'disabled')),
changed_by_type varchar(32) NOT NULL
CONSTRAINT ql3_api_credential_mutations_changed_by_type_check
CHECK (changed_by_type IN ('user', 'system')),
changed_by_id varchar(255) NOT NULL
CONSTRAINT ql3_api_credential_mutations_changed_by_id_check
CHECK (char_length(changed_by_id) >= 1),
audit_event_id uuid NOT NULL,
created_at_ms bigint NOT NULL
CONSTRAINT ql3_api_credential_mutations_created_at_check
CHECK (created_at_ms >= 0),
CONSTRAINT ql3_api_credential_mutations_version_fence_check
CHECK (credential_version = expected_previous_version + 1),
CONSTRAINT ql3_api_credential_mutations_transition_check
CHECK (
operation = 'import'
OR (operation = 'issue' AND expected_previous_version = 0 AND state = 'active')
OR (operation = 'rotate' AND expected_previous_version >= 1 AND state = 'active')
OR (operation = 'revoke' AND expected_previous_version >= 1 AND state = 'revoked')
),
CONSTRAINT ql3_api_credential_mutations_audit_identity_check
CHECK (mutation_id = audit_event_id),
CONSTRAINT ql3_api_credential_mutations_credential_fk
FOREIGN KEY (credential_id, credential_version)
REFERENCES "ql3"."api_credentials" (credential_id, version)
ON DELETE RESTRICT,
CONSTRAINT ql3_api_credential_mutations_subject_fk
FOREIGN KEY (subject_type, subject_id)
REFERENCES "ql3"."identity_subjects" (subject_type, subject_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_api_credential_mutations_audit_fk
FOREIGN KEY (audit_event_id)
REFERENCES "ql3"."security_audit_events" (event_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_api_credential_mutations_credential_version_uidx
UNIQUE (credential_id, credential_version)
)
`.trim(),
`
CREATE INDEX ql3_api_credential_mutations_actor_idx
ON "ql3"."${POSTGRESQL_API_CREDENTIAL_MUTATION_TABLE}"
(changed_by_type, changed_by_id, created_at_ms DESC, mutation_id)
`.trim(),
`
WITH source AS (
SELECT
credential.*,
md5('ql3-credential-import:' || credential.credential_id || ':' || credential.version::text) AS digest
FROM "ql3"."api_credentials" AS credential
), imported AS (
SELECT
source.*,
(
substr(digest, 1, 8) || '-' || substr(digest, 9, 4) || '-4' ||
substr(digest, 14, 3) || '-8' || substr(digest, 18, 3) || '-' ||
substr(digest, 21, 12)
)::uuid AS mutation_id
FROM source
)
INSERT INTO "ql3"."security_audit_events" (
event_id, request_id, operation_id, project_id, subject_type, subject_id,
authentication_id, outcome, reasons, project_version, binding_version,
occurred_at_ms
)
SELECT
mutation_id,
'migration:' || mutation_id::text,
'credential.import',
NULL,
'system',
'pg-0006',
'migration:pg-0006',
'allowed',
'["credential_admin"]'::jsonb,
NULL,
NULL,
created_at_ms
FROM imported
`.trim(),
`
WITH source AS (
SELECT
credential.*,
md5('ql3-credential-import:' || credential.credential_id || ':' || credential.version::text) AS digest
FROM "ql3"."api_credentials" AS credential
), imported AS (
SELECT
source.*,
(
substr(digest, 1, 8) || '-' || substr(digest, 9, 4) || '-4' ||
substr(digest, 14, 3) || '-8' || substr(digest, 18, 3) || '-' ||
substr(digest, 21, 12)
)::uuid AS mutation_id
FROM source
)
INSERT INTO "ql3"."${POSTGRESQL_API_CREDENTIAL_MUTATION_TABLE}" (
mutation_id, operation, credential_id, credential_version,
expected_previous_version, state, subject_type, subject_id,
subject_status, changed_by_type, changed_by_id, audit_event_id, created_at_ms
)
SELECT
mutation_id,
'import',
credential_id,
version,
version - 1,
state,
subject_type,
subject_id,
(
SELECT identity.status
FROM "ql3"."identity_subjects" AS identity
WHERE identity.subject_type = imported.subject_type
AND identity.subject_id = imported.subject_id
),
'system',
'pg-0006',
mutation_id,
created_at_ms
FROM imported
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 5,
migration_id = 'pg-0006-identity-credential-administration',
capabilities = '{"api_credential":1,"api_credential_admin":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 4
AND migration_id = 'pg-0005-api-credential-security-audit'
AND capabilities = '{"api_credential":1,"project_policy":1,"run_core":1,"run_retry_policy":1,"security_audit":1}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 4'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,42 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const pg0007ClusterRecoveryIndexesMigration = definePostgresSqlMigration(
{
id: 'pg-0007-cluster-recovery-indexes',
statements: [
`
CREATE INDEX ql3_runs_runtime_recovery_idx
ON "ql3"."runs" (created_at_ms, id)
WHERE execution_owner = 'runtime'
AND status IN ('created', 'dispatching', 'running')
`.trim(),
`
CREATE INDEX ql3_run_attempts_recovery_idx
ON "ql3"."run_attempts" (lease_expires_at_ms, created_at_ms, id)
WHERE status IN ('claimed', 'starting', 'running')
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 6,
migration_id = 'pg-0007-cluster-recovery-indexes',
capabilities = '{"api_credential":1,"api_credential_admin":1,"cluster_recovery":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 5
AND migration_id = 'pg-0006-identity-credential-administration'
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 5'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
},
);
@@ -0,0 +1,148 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const pg0008RunRecoveryClaimsMigration = definePostgresSqlMigration({
id: 'pg-0008-run-recovery-claims',
statements: [
`
CREATE TABLE "ql3"."run_recovery_controls" (
target_kind varchar(16) NOT NULL,
target_id varchar(36) NOT NULL,
run_id varchar(36) NOT NULL,
attempt_id varchar(36),
target_status varchar(32) NOT NULL,
target_created_at_ms bigint NOT NULL,
observed_at_ms bigint NOT NULL,
state varchar(16) NOT NULL,
claim_owner varchar(128),
claim_token varchar(64),
claim_version integer NOT NULL DEFAULT 0,
claim_expires_at_ms bigint,
next_claim_at_ms bigint,
failure_count integer NOT NULL DEFAULT 0,
created_at_ms bigint NOT NULL,
updated_at_ms bigint NOT NULL,
CONSTRAINT run_recovery_controls_pkey
PRIMARY KEY (target_kind, target_id),
CONSTRAINT ql3_run_recovery_controls_target_kind_check
CHECK (target_kind IN ('run', 'attempt')),
CONSTRAINT ql3_run_recovery_controls_target_id_check
CHECK (char_length(target_id) >= 1),
CONSTRAINT ql3_run_recovery_controls_target_shape_check
CHECK (
(
target_kind = 'run'
AND target_id = run_id
AND attempt_id IS NULL
AND target_status IN ('created', 'dispatching', 'running')
)
OR (
target_kind = 'attempt'
AND target_id = attempt_id
AND attempt_id IS NOT NULL
AND target_status IN ('claimed', 'starting', 'running')
)
),
CONSTRAINT ql3_run_recovery_controls_target_created_at_check
CHECK (target_created_at_ms >= 0),
CONSTRAINT ql3_run_recovery_controls_observed_at_check
CHECK (observed_at_ms >= 0),
CONSTRAINT ql3_run_recovery_controls_state_check
CHECK (state IN ('available', 'claimed', 'retry', 'manual', 'resolved')),
CONSTRAINT ql3_run_recovery_controls_claim_owner_check
CHECK (
claim_owner IS NULL
OR claim_owner ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
),
CONSTRAINT ql3_run_recovery_controls_claim_token_check
CHECK (
claim_token IS NULL
OR claim_token ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{15,63}$'
),
CONSTRAINT ql3_run_recovery_controls_claim_version_check
CHECK (claim_version BETWEEN 0 AND 2147483647),
CONSTRAINT ql3_run_recovery_controls_claim_expires_at_check
CHECK (claim_expires_at_ms IS NULL OR claim_expires_at_ms >= 0),
CONSTRAINT ql3_run_recovery_controls_next_claim_at_check
CHECK (next_claim_at_ms IS NULL OR next_claim_at_ms >= 0),
CONSTRAINT ql3_run_recovery_controls_failure_count_check
CHECK (failure_count BETWEEN 0 AND 2147483647),
CONSTRAINT ql3_run_recovery_controls_created_at_check
CHECK (created_at_ms >= 0),
CONSTRAINT ql3_run_recovery_controls_updated_at_check
CHECK (updated_at_ms >= created_at_ms),
CONSTRAINT ql3_run_recovery_controls_state_shape_check
CHECK (
(
state = 'claimed'
AND claim_owner IS NOT NULL
AND claim_token IS NOT NULL
AND claim_expires_at_ms IS NOT NULL
AND next_claim_at_ms IS NULL
)
OR (
state = 'retry'
AND claim_owner IS NULL
AND claim_token IS NULL
AND claim_expires_at_ms IS NULL
AND next_claim_at_ms IS NOT NULL
)
OR (
state IN ('available', 'manual', 'resolved')
AND claim_owner IS NULL
AND claim_token IS NULL
AND claim_expires_at_ms IS NULL
AND next_claim_at_ms IS NULL
)
),
CONSTRAINT ql3_run_recovery_controls_run_fk
FOREIGN KEY (run_id)
REFERENCES "ql3"."runs" (id)
ON DELETE CASCADE,
CONSTRAINT ql3_run_recovery_controls_attempt_fk
FOREIGN KEY (attempt_id)
REFERENCES "ql3"."run_attempts" (id)
ON DELETE CASCADE
)
`.trim(),
`
CREATE INDEX ql3_run_recovery_controls_available_idx
ON "ql3"."run_recovery_controls"
(target_created_at_ms, target_kind, target_id)
WHERE state IN ('available', 'resolved')
`.trim(),
`
CREATE INDEX ql3_run_recovery_controls_retry_idx
ON "ql3"."run_recovery_controls"
(next_claim_at_ms, target_created_at_ms, target_kind, target_id)
WHERE state = 'retry'
`.trim(),
`
CREATE INDEX ql3_run_recovery_controls_claim_expiry_idx
ON "ql3"."run_recovery_controls"
(claim_expires_at_ms, target_created_at_ms, target_kind, target_id)
WHERE state = 'claimed'
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 7,
migration_id = 'pg-0008-run-recovery-claims',
capabilities = '{"api_credential":1,"api_credential_admin":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 6
AND migration_id = 'pg-0007-cluster-recovery-indexes'
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"cluster_recovery":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 6'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,210 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const pg0009WorkerSessionRunLeaseMigration = definePostgresSqlMigration({
id: 'pg-0009-worker-session-run-lease',
statements: [
`
CREATE TABLE "ql3"."worker_sessions" (
worker_id varchar(128) PRIMARY KEY,
session_id varchar(36) NOT NULL,
generation integer NOT NULL,
status varchar(16) NOT NULL,
version integer NOT NULL,
capabilities_json varchar(16384) NOT NULL,
capabilities_hash char(64) NOT NULL,
max_concurrent_runs integer NOT NULL,
available_slots integer NOT NULL,
registered_at_ms bigint NOT NULL,
last_heartbeat_at_ms bigint NOT NULL,
lease_expires_at_ms bigint NOT NULL,
updated_at_ms bigint NOT NULL,
CONSTRAINT ql3_worker_sessions_worker_id_check
CHECK (worker_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'),
CONSTRAINT ql3_worker_sessions_session_id_check
CHECK (session_id ~ '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
CONSTRAINT ql3_worker_sessions_generation_check
CHECK (generation BETWEEN 1 AND 2147483647),
CONSTRAINT ql3_worker_sessions_status_check
CHECK (status IN ('online', 'draining', 'offline')),
CONSTRAINT ql3_worker_sessions_version_check
CHECK (version BETWEEN 0 AND 2147483647),
CONSTRAINT ql3_worker_sessions_capabilities_check
CHECK (octet_length(capabilities_json) BETWEEN 2 AND 16384),
CONSTRAINT ql3_worker_sessions_capabilities_hash_check
CHECK (capabilities_hash ~ '^[0-9a-f]{64}$'),
CONSTRAINT ql3_worker_sessions_concurrency_check
CHECK (
max_concurrent_runs BETWEEN 1 AND 1024
AND available_slots BETWEEN 0 AND max_concurrent_runs
),
CONSTRAINT ql3_worker_sessions_timestamps_check
CHECK (
registered_at_ms >= 0
AND last_heartbeat_at_ms >= registered_at_ms
AND updated_at_ms >= last_heartbeat_at_ms
AND (
(status = 'offline' AND lease_expires_at_ms >= last_heartbeat_at_ms)
OR (status <> 'offline' AND lease_expires_at_ms > last_heartbeat_at_ms)
)
),
CONSTRAINT ql3_worker_sessions_status_capacity_check
CHECK (status = 'online' OR available_slots = 0)
)
`.trim(),
`
CREATE INDEX ql3_worker_sessions_available_idx
ON "ql3"."worker_sessions" (worker_id)
WHERE status = 'online' AND available_slots > 0
`.trim(),
`
ALTER TABLE "ql3"."run_attempts"
ADD COLUMN worker_session_id varchar(36),
ADD COLUMN worker_generation integer,
ADD COLUMN lease_generation integer,
ADD COLUMN lease_version integer,
ADD COLUMN lease_token_digest char(64),
ADD COLUMN offer_id varchar(128),
ADD CONSTRAINT ql3_run_attempts_worker_session_id_check
CHECK (
worker_session_id IS NULL
OR worker_session_id ~ '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
),
ADD CONSTRAINT ql3_run_attempts_worker_generation_check
CHECK (worker_generation IS NULL OR worker_generation BETWEEN 1 AND 2147483647),
ADD CONSTRAINT ql3_run_attempts_lease_generation_check
CHECK (lease_generation IS NULL OR lease_generation BETWEEN 1 AND 2147483647),
ADD CONSTRAINT ql3_run_attempts_lease_version_check
CHECK (lease_version IS NULL OR lease_version BETWEEN 0 AND 2147483647),
ADD CONSTRAINT ql3_run_attempts_lease_token_digest_check
CHECK (lease_token_digest IS NULL OR lease_token_digest ~ '^[0-9a-f]{64}$'),
ADD CONSTRAINT ql3_run_attempts_offer_id_check
CHECK (offer_id IS NULL OR char_length(offer_id) BETWEEN 1 AND 128),
ADD CONSTRAINT ql3_run_attempts_remote_fence_shape_check
CHECK (
(
worker_session_id IS NULL
AND worker_generation IS NULL
AND lease_generation IS NULL
AND lease_version IS NULL
AND lease_token_digest IS NULL
AND offer_id IS NULL
)
OR (
worker_id IS NOT NULL
AND worker_session_id IS NOT NULL
AND worker_generation IS NOT NULL
AND lease_generation IS NOT NULL
AND lease_version IS NOT NULL
AND lease_token_digest IS NOT NULL
AND offer_id IS NOT NULL
)
)
`.trim(),
`
CREATE TABLE "ql3"."run_dispatch_leases" (
attempt_id varchar(36) PRIMARY KEY,
run_id varchar(36) NOT NULL,
status varchar(16) NOT NULL,
version integer NOT NULL,
lease_generation integer NOT NULL,
worker_id varchar(128) NOT NULL,
worker_session_id varchar(36) NOT NULL,
worker_generation integer NOT NULL,
lease_token_digest char(64) NOT NULL,
offer_id varchar(128) NOT NULL,
acquired_at_ms bigint NOT NULL,
renewed_at_ms bigint NOT NULL,
expires_at_ms bigint NOT NULL,
released_at_ms bigint,
release_reason varchar(32),
completed_at_ms bigint,
updated_at_ms bigint NOT NULL,
CONSTRAINT ql3_run_dispatch_leases_status_check
CHECK (status IN ('leased', 'released', 'completed')),
CONSTRAINT ql3_run_dispatch_leases_version_check
CHECK (version BETWEEN 0 AND 2147483647),
CONSTRAINT ql3_run_dispatch_leases_generation_check
CHECK (lease_generation BETWEEN 1 AND 2147483647),
CONSTRAINT ql3_run_dispatch_leases_worker_generation_check
CHECK (worker_generation BETWEEN 1 AND 2147483647),
CONSTRAINT ql3_run_dispatch_leases_token_digest_check
CHECK (lease_token_digest ~ '^[0-9a-f]{64}$'),
CONSTRAINT ql3_run_dispatch_leases_offer_id_check
CHECK (char_length(offer_id) BETWEEN 1 AND 128),
CONSTRAINT ql3_run_dispatch_leases_timestamps_check
CHECK (
acquired_at_ms >= 0
AND renewed_at_ms >= acquired_at_ms
AND expires_at_ms > renewed_at_ms
AND updated_at_ms >= acquired_at_ms
),
CONSTRAINT ql3_run_dispatch_leases_state_shape_check
CHECK (
(
status = 'leased'
AND released_at_ms IS NULL
AND release_reason IS NULL
AND completed_at_ms IS NULL
)
OR (
status = 'released'
AND released_at_ms IS NOT NULL
AND release_reason IN (
'declined', 'shutdown', 'start_failed', 'capacity_changed', 'lease_expired'
)
AND completed_at_ms IS NULL
)
OR (
status = 'completed'
AND released_at_ms IS NULL
AND release_reason IS NULL
AND completed_at_ms IS NOT NULL
)
),
CONSTRAINT ql3_run_dispatch_leases_attempt_fk
FOREIGN KEY (attempt_id) REFERENCES "ql3"."run_attempts" (id) ON DELETE CASCADE,
CONSTRAINT ql3_run_dispatch_leases_run_fk
FOREIGN KEY (run_id) REFERENCES "ql3"."runs" (id) ON DELETE CASCADE,
CONSTRAINT ql3_run_dispatch_leases_worker_fk
FOREIGN KEY (worker_id) REFERENCES "ql3"."worker_sessions" (worker_id)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_run_dispatch_leases_offer_uidx
ON "ql3"."run_dispatch_leases" (offer_id)
`.trim(),
`
CREATE INDEX ql3_run_dispatch_leases_worker_active_idx
ON "ql3"."run_dispatch_leases"
(worker_id, worker_session_id, worker_generation, expires_at_ms, attempt_id)
WHERE status = 'leased'
`.trim(),
`
CREATE INDEX ql3_run_dispatch_leases_expiry_idx
ON "ql3"."run_dispatch_leases" (expires_at_ms, attempt_id)
WHERE status = 'leased'
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 8,
migration_id = 'pg-0009-worker-session-run-lease',
capabilities = '{"api_credential":1,"api_credential_admin":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"worker_session":1}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 7
AND migration_id = 'pg-0008-run-recovery-claims'
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 7'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,167 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const pg0010WorkerIngressAttestationMigration =
definePostgresSqlMigration({
id: 'pg-0010-worker-ingress-attestation',
statements: [
`
CREATE TABLE "ql3"."worker_credentials" (
credential_id varchar(64) NOT NULL,
version integer NOT NULL,
state varchar(16) NOT NULL,
worker_id varchar(128) NOT NULL,
secret_digest char(64) NOT NULL,
created_at_ms bigint NOT NULL,
not_before_at_ms bigint NOT NULL,
expires_at_ms bigint NOT NULL,
PRIMARY KEY (credential_id, version),
CONSTRAINT ql3_worker_credentials_id_check
CHECK (credential_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$'),
CONSTRAINT ql3_worker_credentials_version_check
CHECK (version BETWEEN 1 AND 2147483647),
CONSTRAINT ql3_worker_credentials_state_check
CHECK (state IN ('active', 'revoked')),
CONSTRAINT ql3_worker_credentials_worker_id_check
CHECK (worker_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'),
CONSTRAINT ql3_worker_credentials_digest_check
CHECK (secret_digest ~ '^[0-9a-f]{64}$'),
CONSTRAINT ql3_worker_credentials_lifetime_check
CHECK (
created_at_ms >= 0
AND not_before_at_ms >= created_at_ms
AND expires_at_ms > not_before_at_ms
)
)
`.trim(),
`
CREATE INDEX ql3_worker_credentials_latest_idx
ON "ql3"."worker_credentials" (credential_id, version DESC)
`.trim(),
`
CREATE TABLE "ql3"."worker_credential_mutations" (
mutation_id varchar(36) PRIMARY KEY,
operation varchar(16) NOT NULL,
credential_id varchar(64) NOT NULL,
credential_version integer NOT NULL,
expected_previous_version integer NOT NULL,
changed_by_type varchar(32) NOT NULL,
changed_by_id varchar(255) NOT NULL,
audit_event_id uuid NOT NULL,
created_at_ms bigint NOT NULL,
CONSTRAINT ql3_worker_credential_mutations_id_check
CHECK (mutation_id ~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
CONSTRAINT ql3_worker_credential_mutations_operation_check
CHECK (operation IN ('issue', 'rotate', 'revoke')),
CONSTRAINT ql3_worker_credential_mutations_version_check
CHECK (
credential_version BETWEEN 1 AND 2147483647
AND expected_previous_version BETWEEN 0 AND 2147483646
AND credential_version = expected_previous_version + 1
),
CONSTRAINT ql3_worker_credential_mutations_actor_check
CHECK (
changed_by_type IN ('user', 'system')
AND char_length(changed_by_id) BETWEEN 1 AND 255
),
CONSTRAINT ql3_worker_credential_mutations_created_at_check
CHECK (created_at_ms >= 0),
CONSTRAINT ql3_worker_credential_mutations_credential_fk
FOREIGN KEY (credential_id, credential_version)
REFERENCES "ql3"."worker_credentials" (credential_id, version),
CONSTRAINT ql3_worker_credential_mutations_audit_fk
FOREIGN KEY (audit_event_id)
REFERENCES "ql3"."security_audit_events" (event_id)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_worker_credential_mutations_audit_uidx
ON "ql3"."worker_credential_mutations" (audit_event_id)
`.trim(),
`
CREATE TABLE "ql3"."worker_execution_attestations" (
attestation_id varchar(36) PRIMARY KEY,
run_id varchar(36) NOT NULL,
attempt_id varchar(36) NOT NULL,
sequence integer NOT NULL,
state varchar(16) NOT NULL,
worker_id varchar(128) NOT NULL,
worker_session_id varchar(36) NOT NULL,
worker_generation integer NOT NULL,
lease_token_digest char(64) NOT NULL,
lease_generation integer NOT NULL,
lease_version integer NOT NULL,
offer_id varchar(128) NOT NULL,
callback_sequence integer NOT NULL,
executor_handle varchar(512) NOT NULL,
journal_revision integer NOT NULL,
received_at_ms bigint NOT NULL,
CONSTRAINT ql3_worker_execution_attestations_id_check
CHECK (attestation_id ~ '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
CONSTRAINT ql3_worker_execution_attestations_sequence_check
CHECK (sequence BETWEEN 1 AND 2147483647),
CONSTRAINT ql3_worker_execution_attestations_state_check
CHECK (state IN ('running', 'stopped')),
CONSTRAINT ql3_worker_execution_attestations_worker_id_check
CHECK (worker_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'),
CONSTRAINT ql3_worker_execution_attestations_session_id_check
CHECK (worker_session_id ~ '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
CONSTRAINT ql3_worker_execution_attestations_generation_check
CHECK (
worker_generation BETWEEN 1 AND 2147483647
AND lease_generation BETWEEN 1 AND 2147483647
AND lease_version BETWEEN 0 AND 2147483647
),
CONSTRAINT ql3_worker_execution_attestations_digest_check
CHECK (lease_token_digest ~ '^[0-9a-f]{64}$'),
CONSTRAINT ql3_worker_execution_attestations_offer_check
CHECK (char_length(offer_id) BETWEEN 1 AND 128),
CONSTRAINT ql3_worker_execution_attestations_callback_check
CHECK (callback_sequence BETWEEN 0 AND 2147483647),
CONSTRAINT ql3_worker_execution_attestations_handle_check
CHECK (char_length(executor_handle) BETWEEN 1 AND 512),
CONSTRAINT ql3_worker_execution_attestations_journal_check
CHECK (journal_revision BETWEEN 1 AND 2147483647),
CONSTRAINT ql3_worker_execution_attestations_received_at_check
CHECK (received_at_ms >= 0),
CONSTRAINT ql3_worker_execution_attestations_run_fk
FOREIGN KEY (run_id) REFERENCES "ql3"."runs" (id) ON DELETE CASCADE,
CONSTRAINT ql3_worker_execution_attestations_attempt_fk
FOREIGN KEY (attempt_id) REFERENCES "ql3"."run_attempts" (id) ON DELETE CASCADE,
CONSTRAINT ql3_worker_execution_attestations_worker_fk
FOREIGN KEY (worker_id) REFERENCES "ql3"."worker_sessions" (worker_id)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_worker_execution_attestations_sequence_uidx
ON "ql3"."worker_execution_attestations"
(attempt_id, lease_generation, sequence)
`.trim(),
`
CREATE INDEX ql3_worker_execution_attestations_exact_idx
ON "ql3"."worker_execution_attestations"
(attempt_id, lease_generation, sequence DESC)
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 9,
migration_id = 'pg-0010-worker-ingress-attestation',
capabilities = '{"api_credential":1,"api_credential_admin":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"worker_attestation":1,"worker_credential":1,"worker_session":1}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 8
AND migration_id = 'pg-0009-worker-session-run-lease'
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"worker_session":1}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 8'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,43 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const pg0011ApiCredentialPepperBindingMigration =
definePostgresSqlMigration({
id: 'pg-0011-api-credential-pepper-binding',
statements: [
`
ALTER TABLE "ql3"."api_credentials"
ADD COLUMN pepper_key_id varchar(64) NOT NULL DEFAULT 'legacy-v1'
`.trim(),
`
ALTER TABLE "ql3"."api_credentials"
ADD CONSTRAINT ql3_api_credentials_pepper_key_id_check
CHECK (pepper_key_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$')
`.trim(),
`
ALTER TABLE "ql3"."api_credentials"
ALTER COLUMN pepper_key_id DROP DEFAULT
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 10,
migration_id = 'pg-0011-api-credential-pepper-binding',
capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"worker_attestation":1,"worker_credential":1,"worker_session":1}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 9
AND migration_id = 'pg-0010-worker-ingress-attestation'
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"worker_attestation":1,"worker_credential":1,"worker_session":1}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 9'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,189 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const pg0012TaskTriggerDefinitionsMigration =
definePostgresSqlMigration({
id: 'pg-0012-task-trigger-definitions',
statements: [
`
CREATE TABLE "ql3"."task_definitions" (
project_id varchar(128) NOT NULL,
task_id varchar(128) NOT NULL,
current_revision integer NOT NULL,
created_at_ms bigint NOT NULL,
updated_at_ms bigint NOT NULL,
CONSTRAINT task_definitions_pkey PRIMARY KEY (project_id, task_id),
CONSTRAINT ql3_task_definitions_project_fk
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id) ON DELETE RESTRICT,
CONSTRAINT ql3_task_definitions_id_check CHECK (
char_length(project_id) BETWEEN 1 AND 128
AND char_length(task_id) BETWEEN 1 AND 128
AND project_id !~ '[[:cntrl:]]'
AND task_id !~ '[[:cntrl:]]'
),
CONSTRAINT ql3_task_definitions_revision_check
CHECK (current_revision BETWEEN 1 AND 2147483647),
CONSTRAINT ql3_task_definitions_time_check
CHECK (created_at_ms >= 0 AND updated_at_ms >= created_at_ms)
)
`.trim(),
`
CREATE TABLE "ql3"."task_definition_revisions" (
project_id varchar(128) NOT NULL,
task_id varchar(128) NOT NULL,
revision integer NOT NULL,
mutation_id uuid NOT NULL,
name varchar(255) NOT NULL,
description varchar(4096),
kind varchar(16) NOT NULL,
spec_json jsonb NOT NULL,
labels_json jsonb NOT NULL,
enabled boolean NOT NULL,
content_digest char(64) NOT NULL,
created_at_ms bigint NOT NULL,
CONSTRAINT task_definition_revisions_pkey
PRIMARY KEY (project_id, task_id, revision),
CONSTRAINT ql3_task_definition_revisions_head_fk
FOREIGN KEY (project_id, task_id)
REFERENCES "ql3"."task_definitions" (project_id, task_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_task_definition_revisions_revision_check
CHECK (revision BETWEEN 1 AND 2147483647),
CONSTRAINT ql3_task_definition_revisions_name_check CHECK (
char_length(name) BETWEEN 1 AND 255
AND (description IS NULL OR char_length(description) BETWEEN 1 AND 4096)
),
CONSTRAINT ql3_task_definition_revisions_kind_check
CHECK (kind IN ('script', 'command', 'workflow', 'agent', 'tool')),
CONSTRAINT ql3_task_definition_revisions_spec_check CHECK (
jsonb_typeof(spec_json) = 'object'
AND octet_length(spec_json::text) BETWEEN 2 AND 65536
),
CONSTRAINT ql3_task_definition_revisions_labels_check CHECK (
jsonb_typeof(labels_json) = 'object'
AND octet_length(labels_json::text) BETWEEN 2 AND 16384
),
CONSTRAINT ql3_task_definition_revisions_digest_check
CHECK (content_digest ~ '^[0-9a-f]{64}$'),
CONSTRAINT ql3_task_definition_revisions_created_check
CHECK (created_at_ms >= 0)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_task_definition_revisions_mutation_uidx
ON "ql3"."task_definition_revisions" (mutation_id)
`.trim(),
`
CREATE INDEX ql3_task_definition_revisions_project_kind_idx
ON "ql3"."task_definition_revisions"
(project_id, kind, enabled, task_id, revision)
`.trim(),
`
CREATE TABLE "ql3"."triggers" (
project_id varchar(128) NOT NULL,
trigger_id varchar(128) NOT NULL,
task_id varchar(128) NOT NULL,
current_revision integer NOT NULL,
created_at_ms bigint NOT NULL,
updated_at_ms bigint NOT NULL,
CONSTRAINT triggers_pkey PRIMARY KEY (project_id, trigger_id),
CONSTRAINT ql3_triggers_project_fk
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id) ON DELETE RESTRICT,
CONSTRAINT ql3_triggers_task_fk
FOREIGN KEY (project_id, task_id)
REFERENCES "ql3"."task_definitions" (project_id, task_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_triggers_id_check CHECK (
char_length(project_id) BETWEEN 1 AND 128
AND char_length(trigger_id) BETWEEN 1 AND 128
AND char_length(task_id) BETWEEN 1 AND 128
AND project_id !~ '[[:cntrl:]]'
AND trigger_id !~ '[[:cntrl:]]'
AND task_id !~ '[[:cntrl:]]'
),
CONSTRAINT ql3_triggers_revision_check
CHECK (current_revision BETWEEN 1 AND 2147483647),
CONSTRAINT ql3_triggers_time_check
CHECK (created_at_ms >= 0 AND updated_at_ms >= created_at_ms)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_triggers_task_uidx
ON "ql3"."triggers" (project_id, trigger_id, task_id)
`.trim(),
`
CREATE TABLE "ql3"."trigger_revisions" (
project_id varchar(128) NOT NULL,
trigger_id varchar(128) NOT NULL,
revision integer NOT NULL,
mutation_id uuid NOT NULL,
task_id varchar(128) NOT NULL,
task_revision integer NOT NULL,
task_content_digest char(64) NOT NULL,
spec_json jsonb NOT NULL,
enabled boolean NOT NULL,
content_digest char(64) NOT NULL,
created_at_ms bigint NOT NULL,
CONSTRAINT trigger_revisions_pkey
PRIMARY KEY (project_id, trigger_id, revision),
CONSTRAINT ql3_trigger_revisions_head_fk
FOREIGN KEY (project_id, trigger_id, task_id)
REFERENCES "ql3"."triggers" (project_id, trigger_id, task_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_trigger_revisions_task_revision_fk
FOREIGN KEY (project_id, task_id, task_revision)
REFERENCES "ql3"."task_definition_revisions" (project_id, task_id, revision)
ON DELETE RESTRICT,
CONSTRAINT ql3_trigger_revisions_revision_check CHECK (
revision BETWEEN 1 AND 2147483647
AND task_revision BETWEEN 1 AND 2147483647
),
CONSTRAINT ql3_trigger_revisions_task_digest_check
CHECK (task_content_digest ~ '^[0-9a-f]{64}$'),
CONSTRAINT ql3_trigger_revisions_spec_check CHECK (
jsonb_typeof(spec_json) = 'object'
AND octet_length(spec_json::text) BETWEEN 2 AND 16384
),
CONSTRAINT ql3_trigger_revisions_digest_check
CHECK (content_digest ~ '^[0-9a-f]{64}$'),
CONSTRAINT ql3_trigger_revisions_created_check
CHECK (created_at_ms >= 0)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_trigger_revisions_mutation_uidx
ON "ql3"."trigger_revisions" (mutation_id)
`.trim(),
`
CREATE INDEX ql3_trigger_revisions_project_enabled_idx
ON "ql3"."trigger_revisions"
(project_id, enabled, trigger_id, revision)
`.trim(),
`
CREATE INDEX ql3_trigger_revisions_task_idx
ON "ql3"."trigger_revisions"
(project_id, task_id, task_revision, trigger_id, revision)
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 11,
migration_id = 'pg-0012-task-trigger-definitions',
capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_session":1}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 10
AND migration_id = 'pg-0011-api-credential-pepper-binding'
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"worker_attestation":1,"worker_credential":1,"worker_session":1}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 10'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,73 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const pg0013TaskExecutionRevisionsMigration =
definePostgresSqlMigration({
id: 'pg-0013-task-execution-revisions',
statements: [
`
CREATE TABLE "ql3"."task_execution_revisions" (
project_id varchar(128) NOT NULL,
task_id varchar(128) NOT NULL,
source_revision integer NOT NULL,
task_revision varchar(96) NOT NULL,
source_content_digest char(64) NOT NULL,
executor_type varchar(32) NOT NULL,
plan_schema varchar(64) NOT NULL,
plan_json jsonb NOT NULL,
content_digest char(64) NOT NULL,
created_at_ms bigint NOT NULL,
CONSTRAINT task_execution_revisions_pkey
PRIMARY KEY (project_id, task_id, source_revision, executor_type),
CONSTRAINT ql3_task_execution_revisions_source_fk
FOREIGN KEY (project_id, task_id, source_revision)
REFERENCES "ql3"."task_definition_revisions"
(project_id, task_id, revision)
ON DELETE RESTRICT,
CONSTRAINT ql3_task_execution_revisions_identity_check CHECK (
task_revision = concat(
'qltd:v1:', source_revision::text, ':', source_content_digest
)
AND source_content_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_task_execution_revisions_executor_check
CHECK (executor_type = 'remote_worker'),
CONSTRAINT ql3_task_execution_revisions_plan_check CHECK (
plan_schema = 'qinglong/command-execution@v1'
AND jsonb_typeof(plan_json) = 'object'
AND octet_length(plan_json::text) BETWEEN 2 AND 98304
),
CONSTRAINT ql3_task_execution_revisions_digest_check
CHECK (content_digest ~ '^[0-9a-f]{64}$'),
CONSTRAINT ql3_task_execution_revisions_created_check
CHECK (created_at_ms >= 0)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_task_execution_revisions_ref_uidx
ON "ql3"."task_execution_revisions"
(project_id, task_id, task_revision, executor_type)
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 12,
migration_id = 'pg-0013-task-execution-revisions',
capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_session":1}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 11
AND migration_id = 'pg-0012-task-trigger-definitions'
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_session":1}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 11'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,110 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const pg0014ClusterSchedulerAdmissionMigration =
definePostgresSqlMigration({
id: 'pg-0014-cluster-scheduler-admission',
statements: [
`
ALTER TABLE "ql3"."runs"
ALTER COLUMN trigger_id TYPE varchar(128)
`.trim(),
`
CREATE TABLE "ql3"."trigger_schedules" (
project_id varchar(128) NOT NULL,
trigger_id varchar(128) NOT NULL,
trigger_revision integer NOT NULL,
next_fire_at_ms bigint,
last_scheduled_at_ms bigint,
state_version integer NOT NULL,
claim_owner varchar(128),
claim_token uuid,
claim_version integer NOT NULL,
claim_expires_at_ms bigint,
updated_at_ms bigint NOT NULL,
CONSTRAINT trigger_schedules_pkey PRIMARY KEY (project_id, trigger_id),
CONSTRAINT ql3_trigger_schedules_revision_fk
FOREIGN KEY (project_id, trigger_id, trigger_revision)
REFERENCES "ql3"."trigger_revisions" (project_id, trigger_id, revision)
ON DELETE RESTRICT,
CONSTRAINT ql3_trigger_schedules_revision_check
CHECK (trigger_revision BETWEEN 1 AND 2147483647),
CONSTRAINT ql3_trigger_schedules_cursor_check CHECK (
next_fire_at_ms IS NULL OR next_fire_at_ms >= 0
),
CONSTRAINT ql3_trigger_schedules_last_check CHECK (
last_scheduled_at_ms IS NULL OR last_scheduled_at_ms >= 0
),
CONSTRAINT ql3_trigger_schedules_version_check CHECK (
state_version BETWEEN 0 AND 2147483647
AND claim_version BETWEEN 0 AND 2147483647
),
CONSTRAINT ql3_trigger_schedules_claim_owner_check CHECK (
claim_owner IS NULL
OR (
char_length(claim_owner) BETWEEN 1 AND 128
AND claim_owner ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
)
),
CONSTRAINT ql3_trigger_schedules_claim_shape_check CHECK (
(
claim_owner IS NULL
AND claim_token IS NULL
AND claim_expires_at_ms IS NULL
)
OR (
claim_owner IS NOT NULL
AND claim_token IS NOT NULL
AND claim_version >= 1
AND claim_expires_at_ms IS NOT NULL
AND claim_expires_at_ms > updated_at_ms
)
),
CONSTRAINT ql3_trigger_schedules_updated_check CHECK (updated_at_ms >= 0)
)
`.trim(),
`
INSERT INTO "ql3"."trigger_schedules" (
project_id, trigger_id, trigger_revision, next_fire_at_ms,
last_scheduled_at_ms, state_version, claim_owner, claim_token,
claim_version, claim_expires_at_ms, updated_at_ms
)
SELECT
project_id, trigger_id, current_revision, NULL, NULL, 0,
NULL, NULL, 0, NULL, updated_at_ms
FROM "ql3"."triggers"
`.trim(),
`
CREATE INDEX ql3_trigger_schedules_due_idx
ON "ql3"."trigger_schedules"
(next_fire_at_ms NULLS FIRST, claim_expires_at_ms, project_id, trigger_id)
`.trim(),
`
CREATE INDEX ql3_trigger_schedules_claim_expiry_idx
ON "ql3"."trigger_schedules"
(claim_expires_at_ms, project_id, trigger_id)
WHERE claim_token IS NOT NULL
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 13,
migration_id = 'pg-0014-cluster-scheduler-admission',
capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_session":1}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 12
AND migration_id = 'pg-0013-task-execution-revisions'
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_session":1}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 12'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,177 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const pg0015WorkerCredentialDeliveryLedgerMigration =
definePostgresSqlMigration({
id: 'pg-0015-worker-credential-delivery-ledger',
statements: [
`
CREATE TABLE "ql3"."worker_credential_deliveries" (
delivery_id varchar(36) NOT NULL,
version integer NOT NULL,
state varchar(32) NOT NULL,
worker_id varchar(128) NOT NULL,
credential_id varchar(64) NOT NULL,
credential_version integer NOT NULL,
previous_credential_id varchar(64),
secret_digest char(64) NOT NULL,
token_digest char(64) NOT NULL,
deployment_target_digest char(64) NOT NULL,
deployment_generation varchar(128) NOT NULL,
staged_at_ms bigint NOT NULL,
credential_committed_at_ms bigint NOT NULL,
published_at_ms bigint,
publication_digest char(64),
observed_at_ms bigint,
observed_session_id varchar(36),
observed_session_version integer,
previous_revoked_at_ms bigint,
CONSTRAINT worker_credential_deliveries_pkey
PRIMARY KEY (delivery_id, version),
CONSTRAINT ql3_worker_credential_deliveries_mutation_fk
FOREIGN KEY (delivery_id)
REFERENCES "ql3"."worker_credential_mutations" (mutation_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_worker_credential_deliveries_credential_fk
FOREIGN KEY (credential_id, credential_version)
REFERENCES "ql3"."worker_credentials" (credential_id, version)
ON DELETE RESTRICT,
CONSTRAINT ql3_worker_credential_deliveries_version_check CHECK (
version BETWEEN 1 AND 4
AND credential_version = 1
),
CONSTRAINT ql3_worker_credential_deliveries_id_check CHECK (
delivery_id ~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
),
CONSTRAINT ql3_worker_credential_deliveries_state_check CHECK (
state IN (
'credential_committed', 'published', 'observed', 'previous_revoked'
)
),
CONSTRAINT ql3_worker_credential_deliveries_worker_check CHECK (
char_length(worker_id) BETWEEN 1 AND 128
AND worker_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
),
CONSTRAINT ql3_worker_credential_deliveries_credential_check CHECK (
char_length(credential_id) BETWEEN 1 AND 64
AND credential_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$'
AND (
previous_credential_id IS NULL
OR (
char_length(previous_credential_id) BETWEEN 1 AND 64
AND previous_credential_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$'
AND previous_credential_id <> credential_id
)
)
),
CONSTRAINT ql3_worker_credential_deliveries_digest_check CHECK (
secret_digest ~ '^[0-9a-f]{64}$'
AND token_digest ~ '^[0-9a-f]{64}$'
AND deployment_target_digest ~ '^[0-9a-f]{64}$'
AND (
publication_digest IS NULL
OR publication_digest ~ '^[0-9a-f]{64}$'
)
),
CONSTRAINT ql3_worker_credential_deliveries_generation_check CHECK (
char_length(deployment_generation) BETWEEN 1 AND 128
AND deployment_generation ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
),
CONSTRAINT ql3_worker_credential_deliveries_session_check CHECK (
observed_session_id IS NULL
OR observed_session_id ~ '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
),
CONSTRAINT ql3_worker_credential_deliveries_time_check CHECK (
staged_at_ms >= 0
AND credential_committed_at_ms >= staged_at_ms
AND (published_at_ms IS NULL OR published_at_ms >= credential_committed_at_ms)
AND (observed_at_ms IS NULL OR observed_at_ms >= published_at_ms)
AND (
previous_revoked_at_ms IS NULL
OR previous_revoked_at_ms >= observed_at_ms
)
),
CONSTRAINT ql3_worker_credential_deliveries_state_shape_check CHECK (
(
version = 1
AND state = 'credential_committed'
AND published_at_ms IS NULL
AND publication_digest IS NULL
AND observed_at_ms IS NULL
AND observed_session_id IS NULL
AND observed_session_version IS NULL
AND previous_revoked_at_ms IS NULL
)
OR (
version = 2
AND state = 'published'
AND published_at_ms IS NOT NULL
AND publication_digest IS NOT NULL
AND observed_at_ms IS NULL
AND observed_session_id IS NULL
AND observed_session_version IS NULL
AND previous_revoked_at_ms IS NULL
)
OR (
version = 3
AND state = 'observed'
AND published_at_ms IS NOT NULL
AND publication_digest IS NOT NULL
AND observed_at_ms IS NOT NULL
AND observed_session_id IS NOT NULL
AND observed_session_version BETWEEN 1 AND 2147483647
AND previous_revoked_at_ms IS NULL
)
OR (
version = 4
AND state = 'previous_revoked'
AND previous_credential_id IS NOT NULL
AND published_at_ms IS NOT NULL
AND publication_digest IS NOT NULL
AND observed_at_ms IS NOT NULL
AND observed_session_id IS NOT NULL
AND observed_session_version BETWEEN 1 AND 2147483647
AND previous_revoked_at_ms IS NOT NULL
)
)
)
`.trim(),
`
CREATE INDEX ql3_worker_credential_deliveries_recovery_idx
ON "ql3"."worker_credential_deliveries"
(state, delivery_id, version DESC)
`.trim(),
`
CREATE UNIQUE INDEX ql3_worker_credential_deliveries_credential_uidx
ON "ql3"."worker_credential_deliveries"
(credential_id, credential_version)
WHERE version = 1
`.trim(),
`
CREATE INDEX ql3_worker_credential_deliveries_credential_idx
ON "ql3"."worker_credential_deliveries"
(credential_id, credential_version, delivery_id, version)
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 14,
migration_id = 'pg-0015-worker-credential-delivery-ledger',
capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_session":1}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 13
AND migration_id = 'pg-0014-cluster-scheduler-admission'
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_session":1}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 13'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,107 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const pg0016WorkerCredentialStageDiscardLedgerMigration =
definePostgresSqlMigration({
id: 'pg-0016-worker-credential-stage-discard-ledger',
statements: [
`
CREATE TABLE "ql3"."worker_credential_stage_discards" (
delivery_id varchar(36) NOT NULL,
version integer NOT NULL,
state varchar(32) NOT NULL,
worker_id varchar(128) NOT NULL,
credential_id varchar(64) NOT NULL,
credential_version integer NOT NULL,
previous_credential_id varchar(64),
secret_digest char(64) NOT NULL,
token_digest char(64) NOT NULL,
deployment_target_digest char(64) NOT NULL,
deployment_generation varchar(128) NOT NULL,
staged_at_ms bigint NOT NULL,
authorized_at_ms bigint NOT NULL,
discarded_at_ms bigint,
CONSTRAINT worker_credential_stage_discards_pkey
PRIMARY KEY (delivery_id, version),
CONSTRAINT ql3_worker_credential_stage_discards_version_check CHECK (
version BETWEEN 1 AND 2
AND credential_version = 1
),
CONSTRAINT ql3_worker_credential_stage_discards_id_check CHECK (
delivery_id ~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
),
CONSTRAINT ql3_worker_credential_stage_discards_state_check CHECK (
state IN ('discard_authorized', 'discarded')
),
CONSTRAINT ql3_worker_credential_stage_discards_worker_check CHECK (
char_length(worker_id) BETWEEN 1 AND 128
AND worker_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
),
CONSTRAINT ql3_worker_credential_stage_discards_credential_check CHECK (
char_length(credential_id) BETWEEN 1 AND 64
AND credential_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$'
AND (
previous_credential_id IS NULL
OR (
char_length(previous_credential_id) BETWEEN 1 AND 64
AND previous_credential_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$'
AND previous_credential_id <> credential_id
)
)
),
CONSTRAINT ql3_worker_credential_stage_discards_digest_check CHECK (
secret_digest ~ '^[0-9a-f]{64}$'
AND token_digest ~ '^[0-9a-f]{64}$'
AND deployment_target_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_worker_credential_stage_discards_generation_check CHECK (
char_length(deployment_generation) BETWEEN 1 AND 128
AND deployment_generation ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
),
CONSTRAINT ql3_worker_credential_stage_discards_time_check CHECK (
staged_at_ms >= 0
AND authorized_at_ms >= 0
AND (discarded_at_ms IS NULL OR discarded_at_ms >= authorized_at_ms)
),
CONSTRAINT ql3_worker_credential_stage_discards_state_shape_check CHECK (
(
version = 1
AND state = 'discard_authorized'
AND discarded_at_ms IS NULL
)
OR (
version = 2
AND state = 'discarded'
AND discarded_at_ms IS NOT NULL
)
)
)
`.trim(),
`
CREATE INDEX ql3_worker_credential_stage_discards_recovery_idx
ON "ql3"."worker_credential_stage_discards"
(state, delivery_id, version DESC)
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 15,
migration_id = 'pg-0016-worker-credential-stage-discard-ledger',
capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 14
AND migration_id = 'pg-0015-worker-credential-delivery-ledger'
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_session":1}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 14'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,102 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const pg0017DatabaseRoleGrantsMigration = definePostgresSqlMigration({
id: 'pg-0017-database-role-grants',
statements: [
`
DO $ql3$
DECLARE
invalid_roles text[];
BEGIN
SELECT array_agg(expected.role_name ORDER BY expected.role_name)
INTO invalid_roles
FROM (
VALUES
('ql3_migration'),
('ql3_runtime'),
('ql3_admin'),
('ql3_worker_ingress')
) AS expected(role_name)
LEFT JOIN pg_catalog.pg_roles roles
ON roles.rolname = expected.role_name
WHERE roles.rolname IS NULL
OR roles.rolcanlogin IS NOT TRUE
OR roles.rolsuper IS NOT FALSE
OR roles.rolcreatedb IS NOT FALSE
OR roles.rolcreaterole IS NOT FALSE
OR roles.rolreplication IS NOT FALSE
OR roles.rolbypassrls IS NOT FALSE;
IF invalid_roles IS NOT NULL THEN
RAISE EXCEPTION 'required QingLong database roles are missing or privileged: %',
invalid_roles
USING ERRCODE = 'insufficient_privilege';
END IF;
END
$ql3$
`.trim(),
'REVOKE ALL ON SCHEMA "ql3" FROM PUBLIC',
'REVOKE ALL ON ALL TABLES IN SCHEMA "ql3" FROM PUBLIC',
`
DO $ql3$
BEGIN
EXECUTE format(
'REVOKE CONNECT ON DATABASE %I FROM PUBLIC',
current_database()
);
EXECUTE format(
'GRANT CONNECT ON DATABASE %I TO ql3_migration, ql3_runtime, ql3_admin, ql3_worker_ingress',
current_database()
);
END
$ql3$
`.trim(),
'GRANT USAGE ON SCHEMA "ql3" TO ql3_runtime',
'GRANT SELECT ON "ql3"."schema_migrations", "ql3"."schema_capabilities" TO ql3_runtime',
'GRANT SELECT, INSERT, UPDATE ON "ql3"."runs", "ql3"."run_attempts", "ql3"."worker_sessions", "ql3"."run_dispatch_leases", "ql3"."run_recovery_controls", "ql3"."run_retry_policies" TO ql3_runtime',
'GRANT SELECT, INSERT ON "ql3"."run_events" TO ql3_runtime',
'GRANT SELECT, INSERT, UPDATE ON "ql3"."projects" TO ql3_runtime',
'GRANT SELECT, INSERT ON "ql3"."project_role_bindings" TO ql3_runtime',
'GRANT SELECT ON "ql3"."identity_subjects", "ql3"."api_credentials", "ql3"."worker_execution_attestations", "ql3"."task_definitions", "ql3"."task_definition_revisions", "ql3"."task_execution_revisions", "ql3"."triggers", "ql3"."trigger_revisions" TO ql3_runtime',
'GRANT SELECT, UPDATE ON "ql3"."trigger_schedules" TO ql3_runtime',
'GRANT INSERT ON "ql3"."security_audit_events" TO ql3_runtime',
'GRANT USAGE ON SCHEMA "ql3" TO ql3_admin',
'GRANT SELECT ON "ql3"."schema_migrations", "ql3"."schema_capabilities" TO ql3_admin',
'GRANT SELECT ON "ql3"."projects" TO ql3_admin',
'GRANT SELECT, INSERT, UPDATE ON "ql3"."identity_subjects" TO ql3_admin',
'GRANT SELECT, INSERT ON "ql3"."api_credentials", "ql3"."security_audit_events", "ql3"."identity_subject_mutations", "ql3"."api_credential_mutations" TO ql3_admin',
'GRANT SELECT, INSERT, UPDATE ON "ql3"."worker_credentials" TO ql3_admin',
'GRANT SELECT, INSERT ON "ql3"."worker_credential_mutations" TO ql3_admin',
'GRANT SELECT, INSERT ON "ql3"."worker_credential_deliveries", "ql3"."worker_credential_stage_discards" TO ql3_admin',
'GRANT SELECT, INSERT, UPDATE ON "ql3"."task_definitions", "ql3"."triggers", "ql3"."trigger_schedules" TO ql3_admin',
'GRANT SELECT, INSERT ON "ql3"."task_definition_revisions", "ql3"."task_execution_revisions", "ql3"."trigger_revisions" TO ql3_admin',
'GRANT USAGE ON SCHEMA "ql3" TO ql3_worker_ingress',
'GRANT SELECT ON "ql3"."schema_migrations", "ql3"."schema_capabilities", "ql3"."run_attempts", "ql3"."run_dispatch_leases", "ql3"."worker_credentials" TO ql3_worker_ingress',
'GRANT SELECT, INSERT, UPDATE ON "ql3"."worker_sessions" TO ql3_worker_ingress',
'GRANT SELECT, INSERT ON "ql3"."worker_credential_deliveries" TO ql3_worker_ingress',
'GRANT SELECT, INSERT ON "ql3"."worker_execution_attestations" TO ql3_worker_ingress',
'GRANT INSERT ON "ql3"."security_audit_events" TO ql3_worker_ingress',
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 16,
migration_id = 'pg-0017-database-role-grants',
capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 15
AND migration_id = 'pg-0016-worker-credential-stage-discard-ledger'
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 15'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,212 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const pg0018PluginPackageInstallsMigration = definePostgresSqlMigration({
id: 'pg-0018-plugin-package-installs',
statements: [
`
CREATE TABLE "ql3"."plugin_package_installs" (
installation_id varchar(128) PRIMARY KEY,
project_id varchar(128) NOT NULL,
package_name varchar(63) NOT NULL,
package_version varchar(128) NOT NULL,
operation varchar(16) NOT NULL,
lock_digest char(64) NOT NULL,
target_generation integer NOT NULL,
previous_active_lock_digest char(64),
active_lock_digest char(64),
state varchar(16) NOT NULL,
version integer NOT NULL,
last_mutation_id varchar(128) NOT NULL,
last_mutation_digest char(64) NOT NULL,
lock_json jsonb NOT NULL,
record_json jsonb NOT NULL,
record_digest char(64) NOT NULL,
created_at_ms bigint NOT NULL,
updated_at_ms bigint NOT NULL,
CONSTRAINT ql3_plugin_package_installs_project_fk
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_installs_identity_check CHECK (
installation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
AND package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$'
AND char_length(package_version) BETWEEN 1 AND 128
AND last_mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
),
CONSTRAINT ql3_plugin_package_installs_operation_check CHECK (
operation IN ('install', 'reinstall', 'upgrade', 'rollback')
),
CONSTRAINT ql3_plugin_package_installs_state_check CHECK (
state IN ('queued', 'staged', 'activating', 'active', 'failed')
),
CONSTRAINT ql3_plugin_package_installs_version_check CHECK (
target_generation BETWEEN 1 AND 2147483647
AND version BETWEEN 1 AND 2147483647
),
CONSTRAINT ql3_plugin_package_installs_digest_check CHECK (
lock_digest ~ '^[0-9a-f]{64}$'
AND (
previous_active_lock_digest IS NULL
OR previous_active_lock_digest ~ '^[0-9a-f]{64}$'
)
AND (
active_lock_digest IS NULL
OR active_lock_digest ~ '^[0-9a-f]{64}$'
)
AND last_mutation_digest ~ '^[0-9a-f]{64}$'
AND record_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_plugin_package_installs_record_check CHECK (
jsonb_typeof(lock_json) = 'object'
AND octet_length(lock_json::text) BETWEEN 2 AND 262144
AND lock_json @> jsonb_build_object(
'lockDigest', lock_digest,
'projectId', project_id,
'packageName', package_name
)
AND jsonb_typeof(record_json) = 'object'
AND octet_length(record_json::text) BETWEEN 2 AND 262144
AND record_json @> jsonb_build_object(
'installationId', installation_id,
'projectId', project_id,
'packageName', package_name,
'lockDigest', lock_digest,
'state', state,
'version', version,
'recordDigest', record_digest
)
),
CONSTRAINT ql3_plugin_package_installs_time_check CHECK (
created_at_ms >= 0
AND updated_at_ms >= created_at_ms
)
)
`.trim(),
`
CREATE TABLE "ql3"."plugin_package_install_heads" (
project_id varchar(128) NOT NULL,
package_name varchar(63) NOT NULL,
installation_id varchar(128) NOT NULL,
CONSTRAINT plugin_package_install_heads_pkey
PRIMARY KEY (project_id, package_name),
CONSTRAINT ql3_plugin_package_install_heads_project_fk
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_install_heads_install_fk
FOREIGN KEY (installation_id)
REFERENCES "ql3"."plugin_package_installs" (installation_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_install_heads_identity_check CHECK (
package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$'
AND installation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_plugin_package_install_heads_install_uidx
ON "ql3"."plugin_package_install_heads" (installation_id)
`.trim(),
`
CREATE TABLE "ql3"."plugin_package_install_mutations" (
installation_id varchar(128) NOT NULL,
mutation_id varchar(128) NOT NULL,
mutation_digest char(64) NOT NULL,
resulting_record_digest char(64) NOT NULL,
occurred_at_ms bigint NOT NULL,
CONSTRAINT plugin_package_install_mutations_pkey
PRIMARY KEY (installation_id, mutation_id),
CONSTRAINT ql3_plugin_package_install_mutations_install_fk
FOREIGN KEY (installation_id)
REFERENCES "ql3"."plugin_package_installs" (installation_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_install_mutations_identity_check CHECK (
mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
),
CONSTRAINT ql3_plugin_package_install_mutations_digest_check CHECK (
mutation_digest ~ '^[0-9a-f]{64}$'
AND resulting_record_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_plugin_package_install_mutations_time_check CHECK (
occurred_at_ms >= 0
)
)
`.trim(),
`
CREATE INDEX ql3_plugin_package_installs_recovery_idx
ON "ql3"."plugin_package_installs" (state, package_name, installation_id)
WHERE state IN ('queued', 'staged', 'activating')
`.trim(),
`
CREATE INDEX ql3_plugin_package_installs_project_history_idx
ON "ql3"."plugin_package_installs"
(project_id, package_name, created_at_ms, installation_id)
`.trim(),
`
CREATE INDEX ql3_plugin_package_install_mutations_result_idx
ON "ql3"."plugin_package_install_mutations"
(installation_id, resulting_record_digest)
`.trim(),
`
CREATE FUNCTION "ql3"."lock_active_plugin_package_project"(varchar)
RETURNS boolean
LANGUAGE plpgsql
VOLATILE
SECURITY DEFINER
SET search_path = pg_catalog, ql3
AS $ql3$
DECLARE
project_is_active boolean;
BEGIN
SELECT projects.status = 'active'
INTO project_is_active
FROM "ql3"."projects" AS projects
WHERE projects.id = $1
FOR SHARE;
RETURN COALESCE(project_is_active, false);
END
$ql3$
`.trim(),
`
REVOKE ALL
ON FUNCTION "ql3"."lock_active_plugin_package_project"(varchar)
FROM PUBLIC
`.trim(),
`
GRANT EXECUTE
ON FUNCTION "ql3"."lock_active_plugin_package_project"(varchar)
TO ql3_admin
`.trim(),
`
GRANT SELECT, INSERT, UPDATE
ON "ql3"."plugin_package_installs", "ql3"."plugin_package_install_heads"
TO ql3_admin
`.trim(),
`
GRANT SELECT, INSERT
ON "ql3"."plugin_package_install_mutations"
TO ql3_admin
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 17,
migration_id = 'pg-0018-plugin-package-installs',
capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_install":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 16
AND migration_id = 'pg-0017-database-role-grants'
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 16'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,253 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const pg0019ApprovedActionsMigration = definePostgresSqlMigration({
id: 'pg-0019-approved-actions',
statements: [
`
CREATE TABLE "ql3"."approval_requests" (
request_id varchar(128) PRIMARY KEY,
project_id varchar(128) NOT NULL,
version integer NOT NULL,
state varchar(16) NOT NULL,
action_type varchar(128) NOT NULL,
action_ref varchar(255) NOT NULL,
action_digest char(64) NOT NULL,
preview_digest char(64) NOT NULL,
requested_by_type varchar(32) NOT NULL,
requested_by_id varchar(255) NOT NULL,
decision_id varchar(128),
consumption_id varchar(128),
dispatch_id varchar(128),
expires_at_ms bigint NOT NULL,
request_json jsonb NOT NULL,
request_digest char(64) NOT NULL,
updated_at_ms bigint NOT NULL,
CONSTRAINT ql3_approval_requests_project_fk
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
ON DELETE RESTRICT,
CONSTRAINT ql3_approval_requests_identity_check CHECK (
request_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
AND action_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
AND action_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$'
),
CONSTRAINT ql3_approval_requests_state_version_check CHECK (
(state = 'pending' AND version = 1)
OR (state IN ('approved', 'rejected') AND version = 2)
OR (state = 'consumed' AND version = 3)
),
CONSTRAINT ql3_approval_requests_digest_check CHECK (
action_digest ~ '^[0-9a-f]{64}$'
AND preview_digest ~ '^[0-9a-f]{64}$'
AND request_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_approval_requests_subject_check CHECK (
requested_by_type IN
('user','api_app','mcp_client','agent','system','worker')
AND char_length(requested_by_id) BETWEEN 1 AND 255
),
CONSTRAINT ql3_approval_requests_mutation_tuple_check CHECK (
(
version = 1 AND decision_id IS NULL
AND consumption_id IS NULL AND dispatch_id IS NULL
)
OR (
version = 2 AND decision_id IS NOT NULL
AND consumption_id IS NULL AND dispatch_id IS NULL
)
OR (
version = 3 AND decision_id IS NOT NULL
AND consumption_id IS NOT NULL AND dispatch_id IS NOT NULL
)
),
CONSTRAINT ql3_approval_requests_json_check CHECK (
jsonb_typeof(request_json) = 'object'
AND octet_length(request_json::text) BETWEEN 2 AND 65536
AND request_json @> jsonb_build_object(
'id', request_id,
'projectId', project_id,
'version', version,
'state', state,
'action', jsonb_build_object(
'actionType', action_type,
'actionRef', action_ref,
'actionDigest', action_digest,
'previewDigest', preview_digest
)
)
),
CONSTRAINT ql3_approval_requests_time_check CHECK (
expires_at_ms > 0 AND updated_at_ms >= 0
)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_approval_requests_decision_uidx
ON "ql3"."approval_requests" (decision_id)
WHERE decision_id IS NOT NULL
`.trim(),
`
CREATE UNIQUE INDEX ql3_approval_requests_consumption_uidx
ON "ql3"."approval_requests" (consumption_id)
WHERE consumption_id IS NOT NULL
`.trim(),
`
CREATE UNIQUE INDEX ql3_approval_requests_dispatch_uidx
ON "ql3"."approval_requests" (dispatch_id)
WHERE dispatch_id IS NOT NULL
`.trim(),
`
CREATE INDEX ql3_approval_requests_pending_idx
ON "ql3"."approval_requests" (expires_at_ms, request_id)
WHERE state = 'pending'
`.trim(),
`
CREATE INDEX ql3_approval_requests_project_idx
ON "ql3"."approval_requests" (project_id, updated_at_ms, request_id)
`.trim(),
`
CREATE TABLE "ql3"."approved_action_dispatches" (
dispatch_id varchar(128) PRIMARY KEY,
approval_request_id varchar(128) NOT NULL,
project_id varchar(128) NOT NULL,
action_type varchar(128) NOT NULL,
action_ref varchar(255) NOT NULL,
action_digest char(64) NOT NULL,
preview_digest char(64) NOT NULL,
dispatch_json jsonb NOT NULL,
dispatch_digest char(64) NOT NULL,
created_at_ms bigint NOT NULL,
CONSTRAINT ql3_approved_action_dispatch_request_fk
FOREIGN KEY (approval_request_id)
REFERENCES "ql3"."approval_requests" (request_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_approved_action_dispatch_project_fk
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
ON DELETE RESTRICT,
CONSTRAINT ql3_approved_action_dispatch_identity_check CHECK (
dispatch_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
AND action_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
AND action_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$'
),
CONSTRAINT ql3_approved_action_dispatch_digest_check CHECK (
action_digest ~ '^[0-9a-f]{64}$'
AND preview_digest ~ '^[0-9a-f]{64}$'
AND dispatch_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_approved_action_dispatch_json_check CHECK (
jsonb_typeof(dispatch_json) = 'object'
AND octet_length(dispatch_json::text) BETWEEN 2 AND 65536
AND dispatch_json @> jsonb_build_object(
'id', dispatch_id,
'approvalRequestId', approval_request_id,
'projectId', project_id,
'action', jsonb_build_object(
'actionType', action_type,
'actionRef', action_ref,
'actionDigest', action_digest,
'previewDigest', preview_digest
)
)
),
CONSTRAINT ql3_approved_action_dispatch_time_check CHECK (
created_at_ms >= 0
)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_approved_action_dispatch_request_uidx
ON "ql3"."approved_action_dispatches" (approval_request_id)
`.trim(),
`
CREATE INDEX ql3_approved_action_dispatch_project_idx
ON "ql3"."approved_action_dispatches"
(project_id, created_at_ms, dispatch_id)
`.trim(),
`
CREATE FUNCTION "ql3"."lock_approval_policy_fence"(
varchar,
varchar,
varchar,
integer,
integer
)
RETURNS boolean
LANGUAGE plpgsql
VOLATILE
SECURITY DEFINER
SET search_path = pg_catalog, ql3
AS $ql3$
DECLARE
current_project_version integer;
current_project_status varchar;
current_binding_version integer;
BEGIN
SELECT project.version, project.status
INTO current_project_version, current_project_status
FROM "ql3"."projects" AS project
WHERE project.id = $1
FOR SHARE;
SELECT binding.version
INTO current_binding_version
FROM "ql3"."project_role_bindings" AS binding
WHERE binding.project_id = $1
AND binding.subject_type = $2
AND binding.subject_id = $3
ORDER BY binding.version DESC
LIMIT 1
FOR SHARE;
RETURN current_project_status = 'active'
AND current_project_version = $4
AND current_binding_version IS NOT DISTINCT FROM $5;
END
$ql3$
`.trim(),
`
REVOKE ALL
ON FUNCTION "ql3"."lock_approval_policy_fence"(
varchar, varchar, varchar, integer, integer
)
FROM PUBLIC
`.trim(),
`
GRANT EXECUTE
ON FUNCTION "ql3"."lock_approval_policy_fence"(
varchar, varchar, varchar, integer, integer
)
TO ql3_admin
`.trim(),
`
GRANT SELECT, INSERT, UPDATE
ON "ql3"."approval_requests"
TO ql3_admin
`.trim(),
`
GRANT SELECT, INSERT
ON "ql3"."approved_action_dispatches"
TO ql3_admin
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 18,
migration_id = 'pg-0019-approved-actions',
capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_install":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 17
AND migration_id = 'pg-0018-plugin-package-installs'
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_install":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 17'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,126 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const pg0020PluginPackageAdmissionReceiptsMigration =
definePostgresSqlMigration({
id: 'pg-0020-plugin-package-admission-receipts',
statements: [
`
CREATE TABLE "ql3"."plugin_package_admission_receipts" (
dispatch_id varchar(128) PRIMARY KEY,
dispatch_digest char(64) NOT NULL,
approval_request_id varchar(128) NOT NULL,
action_ref varchar(255) NOT NULL,
project_id varchar(128) NOT NULL,
package_name varchar(64) NOT NULL,
installation_id varchar(128) NOT NULL,
lock_digest char(64) NOT NULL,
record_digest char(64) NOT NULL,
mutation_id varchar(128) NOT NULL,
mutation_digest char(64) NOT NULL,
audit_event_id uuid NOT NULL,
admitted_at_ms bigint NOT NULL,
receipt_json jsonb NOT NULL,
receipt_digest char(64) NOT NULL,
CONSTRAINT ql3_plugin_package_admission_dispatch_fk
FOREIGN KEY (dispatch_id)
REFERENCES "ql3"."approved_action_dispatches" (dispatch_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_admission_request_fk
FOREIGN KEY (approval_request_id)
REFERENCES "ql3"."approval_requests" (request_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_admission_project_fk
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_admission_install_fk
FOREIGN KEY (installation_id)
REFERENCES "ql3"."plugin_package_installs" (installation_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_admission_audit_fk
FOREIGN KEY (audit_event_id)
REFERENCES "ql3"."security_audit_events" (event_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_admission_identity_check CHECK (
dispatch_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
AND approval_request_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
AND action_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$'
AND project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
AND package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$'
AND installation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
AND mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
),
CONSTRAINT ql3_plugin_package_admission_digest_check CHECK (
dispatch_digest ~ '^[0-9a-f]{64}$'
AND lock_digest ~ '^[0-9a-f]{64}$'
AND record_digest ~ '^[0-9a-f]{64}$'
AND mutation_digest ~ '^[0-9a-f]{64}$'
AND receipt_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_plugin_package_admission_json_check CHECK (
jsonb_typeof(receipt_json) = 'object'
AND octet_length(receipt_json::text) BETWEEN 2 AND 65536
AND receipt_json @> jsonb_build_object(
'schema', 'qinglong/plugin-package-admission-receipt@v1',
'dispatchId', dispatch_id,
'dispatchDigest', dispatch_digest,
'approvalRequestId', approval_request_id,
'actionRef', action_ref,
'projectId', project_id,
'packageName', package_name,
'installationId', installation_id,
'lockDigest', lock_digest,
'recordDigest', record_digest,
'mutationId', mutation_id,
'mutationDigest', mutation_digest,
'auditEventId', audit_event_id::text,
'admittedAtMs', admitted_at_ms,
'receiptDigest', receipt_digest
)
),
CONSTRAINT ql3_plugin_package_admission_time_check CHECK (
admitted_at_ms >= 0
)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_plugin_package_admission_install_uidx
ON "ql3"."plugin_package_admission_receipts" (installation_id)
`.trim(),
`
CREATE UNIQUE INDEX ql3_plugin_package_admission_audit_uidx
ON "ql3"."plugin_package_admission_receipts" (audit_event_id)
`.trim(),
`
CREATE INDEX ql3_plugin_package_admission_project_idx
ON "ql3"."plugin_package_admission_receipts"
(project_id, admitted_at_ms, dispatch_id)
`.trim(),
`
GRANT SELECT, INSERT
ON "ql3"."plugin_package_admission_receipts"
TO ql3_admin
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 19,
migration_id = 'pg-0020-plugin-package-admission-receipts',
capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_install":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 18
AND migration_id = 'pg-0019-approved-actions'
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_install":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 18'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,205 @@
import { definePostgresSqlMigration } from './sqlMigration';
export const pg0021ApprovedActionExecutionsAndPackageProposalsMigration =
definePostgresSqlMigration({
id: 'pg-0021-approved-action-executions-and-package-proposals',
statements: [
`
CREATE TABLE "ql3"."plugin_package_install_proposals" (
action_ref varchar(255) PRIMARY KEY,
project_id varchar(128) NOT NULL,
action_type varchar(128) NOT NULL,
permission varchar(128) NOT NULL,
action_digest char(64) NOT NULL,
preview_digest char(64) NOT NULL,
proposed_by_type varchar(32) NOT NULL,
proposed_by_id varchar(255) NOT NULL,
fence_project_version integer NOT NULL,
fence_binding_version integer,
created_at_ms bigint NOT NULL,
proposal_json jsonb NOT NULL,
proposal_digest char(64) NOT NULL,
CONSTRAINT ql3_plugin_package_proposal_project_fk
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_proposal_identity_check CHECK (
action_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$'
AND action_type = 'plugin_package.install'
AND permission = 'package.manage'
AND proposed_by_type IN
('user','api_app','mcp_client','agent','system','worker')
AND char_length(proposed_by_id) BETWEEN 1 AND 255
),
CONSTRAINT ql3_plugin_package_proposal_digest_check CHECK (
action_digest ~ '^[0-9a-f]{64}$'
AND preview_digest ~ '^[0-9a-f]{64}$'
AND proposal_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_plugin_package_proposal_json_check CHECK (
jsonb_typeof(proposal_json) = 'object'
AND octet_length(proposal_json::text) BETWEEN 2 AND 262144
AND proposal_json @> jsonb_build_object(
'schema', 'qinglong/plugin-package-install-proposal@v1',
'actionRef', action_ref,
'projectId', project_id,
'actionType', action_type,
'permission', permission,
'actionDigest', action_digest,
'previewDigest', preview_digest,
'proposedBy', jsonb_build_object(
'type', proposed_by_type,
'id', proposed_by_id
),
'proposalFence', jsonb_build_object(
'projectVersion', fence_project_version,
'bindingVersion', fence_binding_version
),
'createdAtMs', created_at_ms,
'proposalDigest', proposal_digest
)
),
CONSTRAINT ql3_plugin_package_proposal_time_check CHECK (
fence_project_version > 0
AND (fence_binding_version IS NULL OR fence_binding_version > 0)
AND created_at_ms >= 0
)
)
`.trim(),
`
CREATE INDEX ql3_plugin_package_proposal_project_idx
ON "ql3"."plugin_package_install_proposals"
(project_id, created_at_ms, action_ref)
`.trim(),
`
CREATE TABLE "ql3"."approved_action_executions" (
dispatch_id varchar(128) PRIMARY KEY,
dispatch_digest char(64) NOT NULL,
project_id varchar(128) NOT NULL,
status varchar(16) NOT NULL,
version integer NOT NULL,
attempt_count integer NOT NULL,
max_attempts integer NOT NULL,
eligible_at_ms bigint,
next_attempt_at_ms bigint,
lease_owner varchar(128),
lease_token varchar(128),
lease_expires_at_ms bigint,
started_at_ms bigint,
result_mutation_id varchar(128),
result_code varchar(64),
result_digest char(64),
completed_at_ms bigint,
created_at_ms bigint NOT NULL,
updated_at_ms bigint NOT NULL,
execution_json jsonb NOT NULL,
execution_digest char(64) NOT NULL,
CONSTRAINT ql3_approved_action_execution_dispatch_fk
FOREIGN KEY (dispatch_id)
REFERENCES "ql3"."approved_action_dispatches" (dispatch_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_approved_action_execution_project_fk
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
ON DELETE RESTRICT,
CONSTRAINT ql3_approved_action_execution_state_check CHECK (
status IN
('pending','leased','executing','retry_wait','succeeded','failed','blocked')
AND version BETWEEN 0 AND 2147483647
AND attempt_count BETWEEN 0 AND 16
AND max_attempts BETWEEN 1 AND 16
AND attempt_count <= max_attempts
),
CONSTRAINT ql3_approved_action_execution_lease_check CHECK (
(
lease_owner IS NULL AND lease_token IS NULL
AND lease_expires_at_ms IS NULL
)
OR (
char_length(lease_owner) BETWEEN 1 AND 128
AND char_length(lease_token) BETWEEN 1 AND 128
AND lease_expires_at_ms > updated_at_ms
)
),
CONSTRAINT ql3_approved_action_execution_result_check CHECK (
(
result_mutation_id IS NULL AND result_code IS NULL
)
OR (
char_length(result_mutation_id) BETWEEN 1 AND 128
AND char_length(result_code) BETWEEN 1 AND 64
)
),
CONSTRAINT ql3_approved_action_execution_digest_check CHECK (
dispatch_digest ~ '^[0-9a-f]{64}$'
AND (result_digest IS NULL OR result_digest ~ '^[0-9a-f]{64}$')
AND execution_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_approved_action_execution_json_check CHECK (
jsonb_typeof(execution_json) = 'object'
AND octet_length(execution_json::text) BETWEEN 2 AND 65536
AND execution_json @> jsonb_build_object(
'schema', 'qinglong/approved-action-execution@v1',
'dispatchId', dispatch_id,
'dispatchDigest', dispatch_digest,
'projectId', project_id,
'status', status,
'version', version,
'attemptCount', attempt_count,
'maxAttempts', max_attempts,
'executionDigest', execution_digest
)
),
CONSTRAINT ql3_approved_action_execution_time_check CHECK (
created_at_ms >= 0 AND updated_at_ms >= created_at_ms
)
)
`.trim(),
`
CREATE INDEX ql3_approved_action_execution_due_idx
ON "ql3"."approved_action_executions" (eligible_at_ms, dispatch_id)
WHERE status IN ('pending','leased','retry_wait')
`.trim(),
`
CREATE INDEX ql3_approved_action_execution_recovery_idx
ON "ql3"."approved_action_executions" (lease_expires_at_ms, dispatch_id)
WHERE status = 'executing'
`.trim(),
`
CREATE INDEX ql3_approved_action_execution_project_idx
ON "ql3"."approved_action_executions"
(project_id, updated_at_ms, dispatch_id)
`.trim(),
`
GRANT SELECT, INSERT
ON "ql3"."plugin_package_install_proposals"
TO ql3_admin
`.trim(),
`
GRANT SELECT, INSERT, UPDATE
ON "ql3"."approved_action_executions"
TO ql3_admin
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 20,
migration_id =
'pg-0021-approved-action-executions-and-package-proposals',
capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_install":1,"plugin_package_proposal":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 19
AND migration_id = 'pg-0020-plugin-package-admission-receipts'
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_install":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 19'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,163 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V20 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_install":1,"plugin_package_proposal":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V21 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_install":1,"plugin_package_proposal":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0022PluginPackageAuthoritySplitMigration =
definePostgresSqlMigration({
id: 'pg-0022-plugin-package-authority-split',
statements: [
`
DO $ql3$
DECLARE
invalid_roles text[];
BEGIN
SELECT array_agg(expected.role_name ORDER BY expected.role_name)
INTO invalid_roles
FROM (
VALUES
('ql3_package_manager'),
('ql3_package_executor')
) AS expected(role_name)
LEFT JOIN pg_catalog.pg_roles roles
ON roles.rolname = expected.role_name
WHERE roles.rolname IS NULL
OR roles.rolcanlogin IS NOT TRUE
OR roles.rolsuper IS NOT FALSE
OR roles.rolcreatedb IS NOT FALSE
OR roles.rolcreaterole IS NOT FALSE
OR roles.rolreplication IS NOT FALSE
OR roles.rolbypassrls IS NOT FALSE;
IF invalid_roles IS NOT NULL THEN
RAISE EXCEPTION
'required QingLong Package database roles are missing or privileged: %',
invalid_roles
USING ERRCODE = 'insufficient_privilege';
END IF;
END
$ql3$
`.trim(),
`
DO $ql3$
BEGIN
EXECUTE format(
'GRANT CONNECT ON DATABASE %I TO ql3_package_manager, ql3_package_executor',
current_database()
);
END
$ql3$
`.trim(),
'GRANT USAGE ON SCHEMA "ql3" TO ql3_package_manager, ql3_package_executor',
'GRANT SELECT ON "ql3"."schema_migrations", "ql3"."schema_capabilities" TO ql3_package_manager, ql3_package_executor',
`
REVOKE ALL
ON "ql3"."plugin_package_install_proposals",
"ql3"."approval_requests",
"ql3"."approved_action_dispatches",
"ql3"."approved_action_executions",
"ql3"."plugin_package_installs",
"ql3"."plugin_package_install_heads",
"ql3"."plugin_package_install_mutations",
"ql3"."plugin_package_admission_receipts"
FROM ql3_admin
`.trim(),
`
REVOKE EXECUTE
ON FUNCTION "ql3"."lock_approval_policy_fence"(
varchar, varchar, varchar, integer, integer
)
FROM ql3_admin
`.trim(),
`
REVOKE EXECUTE
ON FUNCTION "ql3"."lock_active_plugin_package_project"(varchar)
FROM ql3_admin
`.trim(),
`
GRANT SELECT
ON "ql3"."projects", "ql3"."project_role_bindings"
TO ql3_package_manager, ql3_package_executor
`.trim(),
`
GRANT SELECT, INSERT
ON "ql3"."security_audit_events"
TO ql3_package_manager, ql3_package_executor
`.trim(),
`
GRANT SELECT, INSERT
ON "ql3"."plugin_package_install_proposals"
TO ql3_package_manager
`.trim(),
`
GRANT SELECT, INSERT, UPDATE
ON "ql3"."approval_requests"
TO ql3_package_manager
`.trim(),
`
GRANT EXECUTE
ON FUNCTION "ql3"."lock_approval_policy_fence"(
varchar, varchar, varchar, integer, integer
)
TO ql3_package_manager, ql3_package_executor
`.trim(),
`
GRANT SELECT
ON "ql3"."plugin_package_install_proposals"
TO ql3_package_executor
`.trim(),
`
GRANT SELECT, UPDATE
ON "ql3"."approval_requests"
TO ql3_package_executor
`.trim(),
`
GRANT SELECT, INSERT
ON "ql3"."approved_action_dispatches"
TO ql3_package_executor
`.trim(),
`
GRANT SELECT, INSERT, UPDATE
ON "ql3"."approved_action_executions",
"ql3"."plugin_package_installs",
"ql3"."plugin_package_install_heads"
TO ql3_package_executor
`.trim(),
`
GRANT SELECT, INSERT
ON "ql3"."plugin_package_install_mutations",
"ql3"."plugin_package_admission_receipts"
TO ql3_package_executor
`.trim(),
`
GRANT EXECUTE
ON FUNCTION "ql3"."lock_active_plugin_package_project"(varchar)
TO ql3_package_executor
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 21,
migration_id = 'pg-0022-plugin-package-authority-split',
capabilities = '${CAPABILITIES_V21}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 20
AND migration_id =
'pg-0021-approved-action-executions-and-package-proposals'
AND capabilities = '${CAPABILITIES_V20}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 20'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,89 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V21 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_install":1,"plugin_package_proposal":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V22 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_proposal":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0023PluginPackageManagementQuotaMigration =
definePostgresSqlMigration({
id: 'pg-0023-plugin-package-management-quota',
statements: [
`
CREATE TABLE "ql3"."plugin_package_management_quota_buckets" (
project_id varchar(128) NOT NULL,
subject_type varchar(32) NOT NULL,
subject_id varchar(255) NOT NULL,
operation varchar(64) NOT NULL,
window_started_at_ms bigint NOT NULL,
consumed_count integer NOT NULL,
receipt_ids jsonb NOT NULL,
updated_at_ms bigint NOT NULL,
CONSTRAINT plugin_package_management_quota_buckets_pkey
PRIMARY KEY (project_id, subject_type, subject_id, operation),
CONSTRAINT ql3_plugin_package_management_quota_project_fk
FOREIGN KEY (project_id)
REFERENCES "ql3"."projects"(id)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_management_quota_identity_check
CHECK (
subject_type = 'user'
AND char_length(subject_id) BETWEEN 1 AND 255
AND subject_id !~ '[[:cntrl:]]'
),
CONSTRAINT ql3_plugin_package_management_quota_operation_check
CHECK (
operation IN (
'plugin-package.propose',
'plugin-package.decide',
'plugin-package.inspect'
)
),
CONSTRAINT ql3_plugin_package_management_quota_window_check
CHECK (
window_started_at_ms >= 0
AND consumed_count BETWEEN 1 AND 1000
AND updated_at_ms >= window_started_at_ms
),
CONSTRAINT ql3_plugin_package_management_quota_receipts_check
CHECK (
jsonb_typeof(receipt_ids) = 'array'
AND jsonb_array_length(receipt_ids) = consumed_count
AND octet_length(receipt_ids::text) BETWEEN 3 AND 262144
)
)
`.trim(),
`
REVOKE ALL
ON "ql3"."plugin_package_management_quota_buckets"
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_executor, ql3_worker_ingress
`.trim(),
`
GRANT SELECT, INSERT, UPDATE
ON "ql3"."plugin_package_management_quota_buckets"
TO ql3_package_manager
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 22,
migration_id = 'pg-0023-plugin-package-management-quota',
capabilities = '${CAPABILITIES_V22}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 21
AND migration_id = 'pg-0022-plugin-package-authority-split'
AND capabilities = '${CAPABILITIES_V21}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 21'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,81 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V22 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_proposal":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V23 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_proposal":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0024PluginPackageIdentityKeysetLedgerMigration =
definePostgresSqlMigration({
id: 'pg-0024-plugin-package-identity-keyset-ledger',
statements: [
`
CREATE TABLE "ql3"."plugin_package_identity_keyset_ledger" (
authority varchar(64) NOT NULL,
generation bigint NOT NULL,
digest varchar(43) NOT NULL,
issuer varchar(512) NOT NULL,
audience varchar(256) NOT NULL,
active_key_ids jsonb NOT NULL,
revoked_key_ids jsonb NOT NULL,
updated_at_ms bigint NOT NULL,
CONSTRAINT plugin_package_identity_keyset_ledger_pkey
PRIMARY KEY (authority),
CONSTRAINT ql3_plugin_package_identity_keyset_authority_check
CHECK (authority = 'plugin-package-management'),
CONSTRAINT ql3_plugin_package_identity_keyset_generation_check
CHECK (generation >= 1 AND updated_at_ms >= 0),
CONSTRAINT ql3_plugin_package_identity_keyset_digest_check
CHECK (digest ~ '^[A-Za-z0-9_-]{43}$'),
CONSTRAINT ql3_plugin_package_identity_keyset_trust_domain_check
CHECK (
char_length(issuer) BETWEEN 1 AND 512
AND issuer !~ '[[:cntrl:]]'
AND char_length(audience) BETWEEN 1 AND 256
AND audience !~ '[[:cntrl:]]'
),
CONSTRAINT ql3_plugin_package_identity_keyset_keys_check
CHECK (
jsonb_typeof(active_key_ids) = 'array'
AND jsonb_array_length(active_key_ids) BETWEEN 1 AND 8
AND octet_length(active_key_ids::text) BETWEEN 3 AND 8192
AND jsonb_typeof(revoked_key_ids) = 'array'
AND jsonb_array_length(revoked_key_ids) BETWEEN 0 AND 64
AND octet_length(revoked_key_ids::text) BETWEEN 2 AND 16384
)
)
`.trim(),
`
REVOKE ALL
ON "ql3"."plugin_package_identity_keyset_ledger"
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_executor, ql3_worker_ingress
`.trim(),
`
GRANT SELECT, INSERT, UPDATE
ON "ql3"."plugin_package_identity_keyset_ledger"
TO ql3_package_manager
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 23,
migration_id = 'pg-0024-plugin-package-identity-keyset-ledger',
capabilities = '${CAPABILITIES_V23}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 22
AND migration_id = 'pg-0023-plugin-package-management-quota'
AND capabilities = '${CAPABILITIES_V22}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 22'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,103 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V23 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_proposal":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V24 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0025PluginPackageMaterializedRevisionsMigration =
definePostgresSqlMigration({
id: 'pg-0025-plugin-package-materialized-revisions',
statements: [
`
CREATE TABLE "ql3"."plugin_package_materialized_revisions" (
generation_digest char(64) NOT NULL,
project_id varchar(128) NOT NULL,
package_name varchar(63) NOT NULL,
generation integer NOT NULL,
lock_digest char(64) NOT NULL,
manifest_digest char(64) NOT NULL,
revision_digest char(64) NOT NULL,
revision_json jsonb NOT NULL,
created_at_ms bigint NOT NULL,
CONSTRAINT plugin_package_materialized_revisions_pkey
PRIMARY KEY (generation_digest),
CONSTRAINT ql3_plugin_package_materialized_revision_project_fk
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_materialized_revision_identity_check CHECK (
package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$' AND
generation BETWEEN 1 AND 2147483647
),
CONSTRAINT ql3_plugin_package_materialized_revision_digest_check CHECK (
generation_digest ~ '^[0-9a-f]{64}$' AND
lock_digest ~ '^[0-9a-f]{64}$' AND
manifest_digest ~ '^[0-9a-f]{64}$' AND
revision_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_plugin_package_materialized_revision_json_check CHECK (
jsonb_typeof(revision_json) = 'object' AND
octet_length(revision_json::text) BETWEEN 2 AND 25165824 AND
revision_json @> jsonb_build_object(
'schema', 'qinglong/plugin-package-materialized-revision@v1',
'generation', jsonb_build_object(
'generationDigest', generation_digest,
'projectId', project_id,
'packageName', package_name,
'generation', generation,
'lockDigest', lock_digest
),
'manifestDigest', manifest_digest,
'revisionDigest', revision_digest
)
),
CONSTRAINT ql3_plugin_package_materialized_revision_time_check CHECK (
created_at_ms >= 0
)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_plugin_package_materialized_revision_generation_uidx
ON "ql3"."plugin_package_materialized_revisions"
(project_id, package_name, generation)
`.trim(),
`
CREATE INDEX ql3_plugin_package_materialized_revision_lock_idx
ON "ql3"."plugin_package_materialized_revisions"
(lock_digest, generation_digest)
`.trim(),
`
REVOKE ALL
ON "ql3"."plugin_package_materialized_revisions"
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_worker_ingress
`.trim(),
`
GRANT SELECT, INSERT
ON "ql3"."plugin_package_materialized_revisions"
TO ql3_package_executor
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 24,
migration_id = 'pg-0025-plugin-package-materialized-revisions',
capabilities = '${CAPABILITIES_V24}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 23
AND migration_id = 'pg-0024-plugin-package-identity-keyset-ledger'
AND capabilities = '${CAPABILITIES_V23}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 23'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,622 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V24 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V25 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0026PluginPackageTaskReconciliationsMigration =
definePostgresSqlMigration({
id: 'pg-0026-plugin-package-task-reconciliations',
statements: [
`
CREATE TABLE "ql3"."plugin_package_task_ownerships" (
project_id varchar(128) NOT NULL,
task_id varchar(128) NOT NULL,
package_name varchar(63) NOT NULL,
claimed_generation_digest char(64) NOT NULL,
created_at_ms bigint NOT NULL,
CONSTRAINT plugin_package_task_ownerships_pkey
PRIMARY KEY (project_id, task_id),
CONSTRAINT ql3_plugin_package_task_ownership_project_fk
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_task_ownership_task_fk
FOREIGN KEY (project_id, task_id)
REFERENCES "ql3"."task_definitions" (project_id, task_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_task_ownership_identity_check CHECK (
char_length(task_id) BETWEEN 1 AND 128 AND
package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$'
),
CONSTRAINT ql3_plugin_package_task_ownership_digest_check CHECK (
claimed_generation_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_plugin_package_task_ownership_namespace_check CHECK (
task_id LIKE 'pkg:' || package_name || ':%'
),
CONSTRAINT ql3_plugin_package_task_ownership_time_check CHECK (
created_at_ms >= 0
)
)
`.trim(),
`
CREATE INDEX ql3_plugin_package_task_ownership_package_idx
ON "ql3"."plugin_package_task_ownerships"
(project_id, package_name, task_id)
`.trim(),
`
CREATE TABLE "ql3"."plugin_package_task_reconciliations" (
generation_digest char(64) NOT NULL,
project_id varchar(128) NOT NULL,
package_name varchar(63) NOT NULL,
generation integer NOT NULL,
materialized_revision_digest char(64) NOT NULL,
lock_digest char(64) NOT NULL,
previous_lock_digest char(64),
receipt_digest char(64) NOT NULL,
receipt_json jsonb NOT NULL,
committed_at_ms bigint NOT NULL,
CONSTRAINT plugin_package_task_reconciliations_pkey
PRIMARY KEY (generation_digest),
CONSTRAINT ql3_plugin_package_task_reconciliation_materialized_fk
FOREIGN KEY (generation_digest)
REFERENCES "ql3"."plugin_package_materialized_revisions"
(generation_digest)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_task_reconciliation_identity_check CHECK (
package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$' AND
generation BETWEEN 1 AND 2147483647
),
CONSTRAINT ql3_plugin_package_task_reconciliation_digest_check CHECK (
generation_digest ~ '^[0-9a-f]{64}$' AND
materialized_revision_digest ~ '^[0-9a-f]{64}$' AND
lock_digest ~ '^[0-9a-f]{64}$' AND
(previous_lock_digest IS NULL OR
previous_lock_digest ~ '^[0-9a-f]{64}$') AND
receipt_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_plugin_package_task_reconciliation_json_check CHECK (
jsonb_typeof(receipt_json) = 'object' AND
octet_length(receipt_json::text) BETWEEN 2 AND 8388608 AND
receipt_json @> jsonb_build_object(
'schema', 'qinglong/plugin-package-task-reconciliation@v1',
'generationDigest', generation_digest,
'projectId', project_id,
'packageName', package_name,
'generation', generation,
'materializedRevisionDigest', materialized_revision_digest,
'lockDigest', lock_digest,
'receiptDigest', receipt_digest
)
),
CONSTRAINT ql3_plugin_package_task_reconciliation_time_check CHECK (
committed_at_ms >= 0
)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_plugin_package_task_reconciliation_generation_uidx
ON "ql3"."plugin_package_task_reconciliations"
(project_id, package_name, generation)
`.trim(),
`
CREATE INDEX ql3_plugin_package_task_reconciliation_lock_idx
ON "ql3"."plugin_package_task_reconciliations"
(lock_digest, generation_digest)
`.trim(),
`
CREATE TABLE "ql3"."plugin_package_task_reconciliation_items" (
generation_digest char(64) NOT NULL,
task_id varchar(128) NOT NULL,
revision integer NOT NULL,
disposition varchar(24) NOT NULL,
content_digest char(64) NOT NULL,
CONSTRAINT plugin_package_task_reconciliation_items_pkey
PRIMARY KEY (generation_digest, task_id),
CONSTRAINT ql3_plugin_package_task_reconciliation_item_reconciliation_fk
FOREIGN KEY (generation_digest)
REFERENCES "ql3"."plugin_package_task_reconciliations"
(generation_digest)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_task_reconciliation_item_identity_check CHECK (
char_length(task_id) BETWEEN 1 AND 128 AND
revision BETWEEN 1 AND 2147483647
),
CONSTRAINT ql3_plugin_package_task_reconciliation_item_disposition_check
CHECK (disposition IN (
'already_disabled','created','disabled','retained','updated'
)),
CONSTRAINT ql3_plugin_package_task_reconciliation_item_digest_check CHECK (
content_digest ~ '^[0-9a-f]{64}$'
)
)
`.trim(),
`
CREATE INDEX ql3_plugin_package_task_reconciliation_item_task_idx
ON "ql3"."plugin_package_task_reconciliation_items"
(task_id, generation_digest)
`.trim(),
`
CREATE FUNCTION "ql3"."commit_plugin_package_task_reconciliation"(
p_generation_digest char(64),
p_materialized_revision_digest char(64),
p_receipt jsonb,
p_writes jsonb,
p_execution_revisions jsonb
)
RETURNS boolean
LANGUAGE plpgsql
VOLATILE
SECURITY DEFINER
SET search_path = pg_catalog, ql3
AS $ql3$
DECLARE
materialized "ql3"."plugin_package_materialized_revisions"%ROWTYPE;
install_record "ql3"."plugin_package_installs"%ROWTYPE;
previous_receipt jsonb;
expected_ids text[];
receipt_ids text[];
write_ids text[];
execution_ids text[];
task_id_value text;
item jsonb;
write_value jsonb;
definition jsonb;
execution_value jsonb;
resource_value jsonb;
current_head "ql3"."task_definitions"%ROWTYPE;
current_revision "ql3"."task_definition_revisions"%ROWTYPE;
owner_package varchar(63);
now_ms bigint;
BEGIN
IF NOT pg_has_role(session_user, 'ql3_package_executor', 'member') THEN
RAISE EXCEPTION 'Package Task reconciliation authority is required'
USING ERRCODE = 'insufficient_privilege';
END IF;
IF p_generation_digest !~ '^[0-9a-f]{64}$'
OR p_materialized_revision_digest !~ '^[0-9a-f]{64}$'
OR jsonb_typeof(p_receipt) <> 'object'
OR jsonb_typeof(p_writes) <> 'array'
OR jsonb_typeof(p_execution_revisions) <> 'array'
OR jsonb_array_length(p_writes) > 512
OR jsonb_array_length(p_execution_revisions) > 512 THEN
RAISE EXCEPTION 'invalid Package Task reconciliation input'
USING ERRCODE = 'check_violation';
END IF;
SELECT * INTO materialized
FROM "ql3"."plugin_package_materialized_revisions"
WHERE generation_digest = p_generation_digest
FOR KEY SHARE;
IF NOT FOUND
OR materialized.revision_digest <> p_materialized_revision_digest THEN
RAISE EXCEPTION 'materialized revision fence changed'
USING ERRCODE = 'serialization_failure';
END IF;
SELECT install.* INTO install_record
FROM "ql3"."plugin_package_install_heads" AS head
JOIN "ql3"."plugin_package_installs" AS install
ON install.installation_id = head.installation_id
WHERE head.project_id = materialized.project_id
AND head.package_name = materialized.package_name
FOR UPDATE OF install;
IF NOT FOUND
OR install_record.state <> 'active'
OR install_record.installation_id <>
materialized.revision_json #>> '{generation,installationId}'
OR install_record.target_generation <> materialized.generation
OR install_record.lock_digest <> materialized.lock_digest
OR install_record.previous_active_lock_digest IS DISTINCT FROM
NULLIF(materialized.revision_json #>> '{generation,previousActiveLockDigest}', '')
THEN
RAISE EXCEPTION 'active Package install fence changed'
USING ERRCODE = 'serialization_failure';
END IF;
IF EXISTS (
SELECT 1 FROM "ql3"."plugin_package_task_reconciliations"
WHERE generation_digest = p_generation_digest
AND receipt_json = p_receipt
) THEN
RETURN false;
ELSIF EXISTS (
SELECT 1 FROM "ql3"."plugin_package_task_reconciliations"
WHERE generation_digest = p_generation_digest
) THEN
RAISE EXCEPTION 'generation receipt identity is already bound'
USING ERRCODE = 'unique_violation';
END IF;
IF materialized.generation = 1 THEN
IF materialized.revision_json #>> '{generation,previousActiveLockDigest}'
IS NOT NULL THEN
RAISE EXCEPTION 'first generation has a previous lock'
USING ERRCODE = 'check_violation';
END IF;
previous_receipt := NULL;
ELSE
SELECT receipt_json INTO previous_receipt
FROM "ql3"."plugin_package_task_reconciliations"
WHERE project_id = materialized.project_id
AND package_name = materialized.package_name
AND generation = materialized.generation - 1
FOR KEY SHARE;
IF NOT FOUND
OR previous_receipt ->> 'lockDigest' <>
materialized.revision_json #>>
'{generation,previousActiveLockDigest}' THEN
RAISE EXCEPTION 'previous generation receipt is missing'
USING ERRCODE = 'serialization_failure';
END IF;
END IF;
IF p_receipt ->> 'schema' <>
'qinglong/plugin-package-task-reconciliation@v1'
OR p_receipt ->> 'generationDigest' <> p_generation_digest
OR p_receipt ->> 'materializedRevisionDigest' <>
p_materialized_revision_digest
OR p_receipt ->> 'projectId' <> materialized.project_id
OR p_receipt ->> 'packageName' <> materialized.package_name
OR (p_receipt ->> 'generation')::integer <> materialized.generation
OR p_receipt ->> 'lockDigest' <> materialized.lock_digest
OR jsonb_typeof(p_receipt -> 'items') <> 'array'
OR jsonb_array_length(p_receipt -> 'items') > 512 THEN
RAISE EXCEPTION 'receipt does not match materialized generation'
USING ERRCODE = 'check_violation';
END IF;
SELECT array_agg(value ORDER BY value) INTO expected_ids
FROM (
SELECT 'pkg:' || materialized.package_name || ':' ||
(resource_json -> 'value' ->> 'id') AS value
FROM jsonb_array_elements(materialized.revision_json -> 'resources')
AS resources(resource_json)
WHERE resource_json ->> 'kind' = 'task'
UNION
SELECT previous_item_json ->> 'taskId'
FROM jsonb_array_elements(COALESCE(previous_receipt -> 'items', '[]'::jsonb))
AS previous_items(previous_item_json)
) AS expected;
expected_ids := COALESCE(expected_ids, ARRAY[]::text[]);
SELECT array_agg(value ORDER BY value), count(DISTINCT value)
INTO receipt_ids, now_ms
FROM (
SELECT receipt_item_json ->> 'taskId' AS value
FROM jsonb_array_elements(p_receipt -> 'items')
AS receipt_items(receipt_item_json)
) AS receipt_values;
receipt_ids := COALESCE(receipt_ids, ARRAY[]::text[]);
IF receipt_ids <> expected_ids
OR now_ms <> cardinality(receipt_ids) THEN
RAISE EXCEPTION 'receipt does not exactly cover the generation task set'
USING ERRCODE = 'check_violation';
END IF;
SELECT array_agg(value ORDER BY value), count(DISTINCT value)
INTO write_ids, now_ms
FROM (
SELECT write_entry_json -> 'definition' ->> 'taskId' AS value
FROM jsonb_array_elements(p_writes) AS writes(write_entry_json)
) AS write_values;
write_ids := COALESCE(write_ids, ARRAY[]::text[]);
IF now_ms <> cardinality(write_ids)
OR NOT write_ids <@ expected_ids THEN
RAISE EXCEPTION 'writes are duplicated or outside the generation task set'
USING ERRCODE = 'check_violation';
END IF;
SELECT array_agg(value ORDER BY value), count(DISTINCT value)
INTO execution_ids, now_ms
FROM (
SELECT execution_entry_json ->> 'taskId' AS value
FROM jsonb_array_elements(p_execution_revisions)
AS executions(execution_entry_json)
) AS execution_values;
execution_ids := COALESCE(execution_ids, ARRAY[]::text[]);
IF now_ms <> cardinality(execution_ids)
OR NOT execution_ids <@ write_ids THEN
RAISE EXCEPTION 'execution revisions are duplicated or unbound'
USING ERRCODE = 'check_violation';
END IF;
FOREACH task_id_value IN ARRAY expected_ids LOOP
SELECT receipt_item_json INTO item
FROM jsonb_array_elements(p_receipt -> 'items')
AS receipt_items(receipt_item_json)
WHERE receipt_item_json ->> 'taskId' = task_id_value;
SELECT write_entry_json INTO write_value
FROM jsonb_array_elements(p_writes) AS writes(write_entry_json)
WHERE write_entry_json -> 'definition' ->> 'taskId' = task_id_value;
definition := write_value -> 'definition';
SELECT resource_json -> 'value' INTO resource_value
FROM jsonb_array_elements(materialized.revision_json -> 'resources')
AS resources(resource_json)
WHERE resource_json ->> 'kind' = 'task'
AND 'pkg:' || materialized.package_name || ':' ||
(resource_json -> 'value' ->> 'id') = task_id_value;
SELECT * INTO current_head
FROM "ql3"."task_definitions"
WHERE project_id = materialized.project_id AND task_id = task_id_value
FOR UPDATE;
IF FOUND THEN
SELECT * INTO current_revision
FROM "ql3"."task_definition_revisions"
WHERE project_id = current_head.project_id
AND task_id = current_head.task_id
AND revision = current_head.current_revision;
SELECT package_name INTO owner_package
FROM "ql3"."plugin_package_task_ownerships"
WHERE project_id = current_head.project_id
AND task_id = current_head.task_id;
IF owner_package IS DISTINCT FROM materialized.package_name THEN
RAISE EXCEPTION 'TaskDefinition ownership collision'
USING ERRCODE = 'unique_violation';
END IF;
ELSE
current_revision := NULL;
owner_package := NULL;
END IF;
IF item ->> 'disposition' IN ('created','updated','disabled') THEN
IF write_value IS NULL
OR item ->> 'contentDigest' <> definition ->> 'contentDigest'
OR (item ->> 'revision')::integer <>
(definition ->> 'revision')::integer
OR write_value -> 'command' ->> 'projectId' <>
materialized.project_id
OR write_value -> 'command' ->> 'taskId' <> task_id_value
OR write_value -> 'command' ->> 'mutationId' <>
definition ->> 'mutationId' THEN
RAISE EXCEPTION 'receipt write binding is invalid'
USING ERRCODE = 'check_violation';
END IF;
ELSIF write_value IS NOT NULL THEN
RAISE EXCEPTION 'non-mutating receipt item has a write'
USING ERRCODE = 'check_violation';
END IF;
IF resource_value IS NOT NULL THEN
IF item ->> 'disposition' NOT IN ('created','retained','updated') THEN
RAISE EXCEPTION 'active materialized Task has an invalid disposition'
USING ERRCODE = 'check_violation';
END IF;
IF write_value IS NOT NULL AND (
definition ->> 'projectId' <> materialized.project_id OR
definition ->> 'taskId' <> task_id_value OR
definition ->> 'name' <> resource_value ->> 'name' OR
definition -> 'description' IS DISTINCT FROM
resource_value -> 'description' OR
definition ->> 'kind' <> resource_value ->> 'kind' OR
definition -> 'spec' IS DISTINCT FROM resource_value -> 'spec' OR
definition -> 'labels' IS DISTINCT FROM resource_value -> 'labels' OR
definition -> 'enabled' IS DISTINCT FROM resource_value -> 'enabled'
) THEN
RAISE EXCEPTION 'Task write differs from materialized resource'
USING ERRCODE = 'check_violation';
END IF;
ELSE
IF item ->> 'disposition' NOT IN ('already_disabled','disabled') THEN
RAISE EXCEPTION 'removed Task has an invalid disposition'
USING ERRCODE = 'check_violation';
END IF;
IF item ->> 'disposition' = 'disabled' AND (
current_revision IS NULL OR
definition ->> 'name' <> current_revision.name OR
definition -> 'description' IS DISTINCT FROM
to_jsonb(current_revision.description) OR
definition ->> 'kind' <> current_revision.kind OR
definition -> 'spec' IS DISTINCT FROM current_revision.spec_json OR
definition -> 'labels' IS DISTINCT FROM current_revision.labels_json OR
definition -> 'enabled' <> 'false'
) THEN
RAISE EXCEPTION 'removed Task disable write is invalid'
USING ERRCODE = 'check_violation';
END IF;
END IF;
IF write_value IS NULL THEN
IF current_revision IS NULL
OR (item ->> 'revision')::integer <> current_revision.revision
OR item ->> 'contentDigest' <> current_revision.content_digest THEN
RAISE EXCEPTION 'retained Task head changed'
USING ERRCODE = 'serialization_failure';
END IF;
ELSIF current_revision IS NULL THEN
IF item ->> 'disposition' <> 'created'
OR write_value -> 'command' -> 'expectedRevision' <> 'null'::jsonb
OR (definition ->> 'revision')::integer <> 1 THEN
RAISE EXCEPTION 'Task create CAS is invalid'
USING ERRCODE = 'serialization_failure';
END IF;
ELSIF item ->> 'disposition' = 'created'
OR (write_value -> 'command' ->> 'expectedRevision')::integer <>
current_revision.revision
OR (definition ->> 'revision')::integer <>
current_revision.revision + 1 THEN
RAISE EXCEPTION 'Task update CAS is invalid'
USING ERRCODE = 'serialization_failure';
END IF;
execution_value := NULL;
SELECT execution_entry_json INTO execution_value
FROM jsonb_array_elements(p_execution_revisions)
AS executions(execution_entry_json)
WHERE execution_entry_json ->> 'taskId' = task_id_value;
IF write_value IS NOT NULL
AND definition -> 'enabled' = 'true'::jsonb
AND definition ->> 'kind' = 'command'
AND definition #>> '{spec,schema}' = 'qinglong/command@v1' THEN
IF execution_value IS NULL
OR (execution_value ->> 'sourceRevision')::integer <>
(definition ->> 'revision')::integer
OR execution_value ->> 'sourceContentDigest' <>
definition ->> 'contentDigest' THEN
RAISE EXCEPTION 'required execution revision is missing'
USING ERRCODE = 'check_violation';
END IF;
ELSIF execution_value IS NOT NULL THEN
RAISE EXCEPTION 'unexpected execution revision'
USING ERRCODE = 'check_violation';
END IF;
END LOOP;
now_ms := floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint;
FOREACH task_id_value IN ARRAY write_ids LOOP
SELECT write_entry_json INTO write_value
FROM jsonb_array_elements(p_writes) AS writes(write_entry_json)
WHERE write_entry_json -> 'definition' ->> 'taskId' = task_id_value;
definition := write_value -> 'definition';
IF write_value -> 'command' -> 'expectedRevision' = 'null'::jsonb THEN
INSERT INTO "ql3"."task_definitions" (
project_id, task_id, current_revision, created_at_ms, updated_at_ms
) VALUES (
definition ->> 'projectId', definition ->> 'taskId', 1,
(definition ->> 'createdAtMs')::bigint,
(definition ->> 'updatedAtMs')::bigint
);
INSERT INTO "ql3"."plugin_package_task_ownerships" (
project_id, task_id, package_name,
claimed_generation_digest, created_at_ms
) VALUES (
materialized.project_id, task_id_value, materialized.package_name,
p_generation_digest, now_ms
);
END IF;
INSERT INTO "ql3"."task_definition_revisions" (
project_id, task_id, revision, mutation_id, name, description, kind,
spec_json, labels_json, enabled, content_digest, created_at_ms
) VALUES (
definition ->> 'projectId', definition ->> 'taskId',
(definition ->> 'revision')::integer,
(definition ->> 'mutationId')::uuid,
definition ->> 'name', definition ->> 'description',
definition ->> 'kind', definition -> 'spec', definition -> 'labels',
(definition ->> 'enabled')::boolean, definition ->> 'contentDigest',
(definition ->> 'updatedAtMs')::bigint
);
SELECT execution_entry_json INTO execution_value
FROM jsonb_array_elements(p_execution_revisions)
AS executions(execution_entry_json)
WHERE execution_entry_json ->> 'taskId' = task_id_value;
IF execution_value IS NOT NULL THEN
INSERT INTO "ql3"."task_execution_revisions" (
project_id, task_id, source_revision, task_revision,
source_content_digest, executor_type, plan_schema, plan_json,
content_digest, created_at_ms
) VALUES (
execution_value ->> 'projectId', execution_value ->> 'taskId',
(execution_value ->> 'sourceRevision')::integer,
execution_value ->> 'taskRevision',
execution_value ->> 'sourceContentDigest',
execution_value ->> 'executorType',
execution_value ->> 'planSchema',
execution_value -> 'planJson',
execution_value ->> 'contentDigest',
(execution_value ->> 'createdAtMs')::bigint
);
END IF;
IF write_value -> 'command' -> 'expectedRevision' <> 'null'::jsonb THEN
UPDATE "ql3"."task_definitions" AS task_head
SET current_revision = (definition ->> 'revision')::integer,
updated_at_ms = (definition ->> 'updatedAtMs')::bigint
WHERE task_head.project_id = materialized.project_id
AND task_head.task_id = task_id_value
AND task_head.current_revision =
(write_value -> 'command' ->> 'expectedRevision')::integer;
IF NOT FOUND THEN
RAISE EXCEPTION 'Task head changed while committing'
USING ERRCODE = 'serialization_failure';
END IF;
END IF;
END LOOP;
INSERT INTO "ql3"."plugin_package_task_reconciliations" (
generation_digest, project_id, package_name, generation,
materialized_revision_digest, lock_digest, previous_lock_digest,
receipt_digest, receipt_json, committed_at_ms
) VALUES (
p_generation_digest, materialized.project_id, materialized.package_name,
materialized.generation, p_materialized_revision_digest,
materialized.lock_digest,
NULLIF(p_receipt ->> 'previousLockDigest', ''),
p_receipt ->> 'receiptDigest', p_receipt,
(p_receipt ->> 'committedAtMs')::bigint
);
INSERT INTO "ql3"."plugin_package_task_reconciliation_items" (
generation_digest, task_id, revision, disposition, content_digest
)
SELECT p_generation_digest, receipt_item_json ->> 'taskId',
(receipt_item_json ->> 'revision')::integer,
receipt_item_json ->> 'disposition',
receipt_item_json ->> 'contentDigest'
FROM jsonb_array_elements(p_receipt -> 'items')
AS receipt_items(receipt_item_json);
RETURN true;
END
$ql3$
`.trim(),
`
REVOKE ALL
ON "ql3"."plugin_package_task_ownerships",
"ql3"."plugin_package_task_reconciliations",
"ql3"."plugin_package_task_reconciliation_items"
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_package_executor, ql3_worker_ingress
`.trim(),
`
GRANT SELECT
ON "ql3"."plugin_package_task_ownerships"
TO ql3_admin, ql3_package_executor
`.trim(),
`
GRANT SELECT
ON "ql3"."plugin_package_task_reconciliations",
"ql3"."plugin_package_task_reconciliation_items",
"ql3"."task_definitions",
"ql3"."task_definition_revisions",
"ql3"."task_execution_revisions"
TO ql3_package_executor
`.trim(),
`
REVOKE ALL
ON FUNCTION "ql3"."commit_plugin_package_task_reconciliation"(
char(64), char(64), jsonb, jsonb, jsonb
)
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_worker_ingress
`.trim(),
`
GRANT EXECUTE
ON FUNCTION "ql3"."commit_plugin_package_task_reconciliation"(
char(64), char(64), jsonb, jsonb, jsonb
)
TO ql3_package_executor
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 25,
migration_id = 'pg-0026-plugin-package-task-reconciliations',
capabilities = '${CAPABILITIES_V25}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 24
AND migration_id = 'pg-0025-plugin-package-materialized-revisions'
AND capabilities = '${CAPABILITIES_V24}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 24'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,163 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V25 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V26 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0027ProjectToolDefinitionSnapshotsMigration =
definePostgresSqlMigration({
id: 'pg-0027-project-tool-definition-snapshots',
statements: [
`
CREATE UNIQUE INDEX ql3_plugin_package_installs_snapshot_source_uidx
ON "ql3"."plugin_package_installs"
(project_id, package_name, installation_id, target_generation, lock_digest)
`.trim(),
`
CREATE UNIQUE INDEX ql3_plugin_package_materialized_revision_snapshot_source_uidx
ON "ql3"."plugin_package_materialized_revisions"
(project_id, package_name, generation, generation_digest,
lock_digest, revision_digest)
`.trim(),
`
CREATE TABLE "ql3"."project_tool_definition_snapshots" (
project_id varchar(128) NOT NULL,
active_vector_digest char(64) NOT NULL,
definitions_digest char(64) NOT NULL,
snapshot_digest char(64) NOT NULL,
snapshot_json jsonb NOT NULL,
committed_at_ms bigint NOT NULL,
CONSTRAINT project_tool_definition_snapshots_pkey
PRIMARY KEY (project_id, active_vector_digest),
CONSTRAINT ql3_project_tool_definition_snapshot_project_fk
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
ON DELETE RESTRICT,
CONSTRAINT ql3_project_tool_definition_snapshot_identity_check CHECK (
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
),
CONSTRAINT ql3_project_tool_definition_snapshot_digest_check CHECK (
active_vector_digest ~ '^[0-9a-f]{64}$' AND
definitions_digest ~ '^[0-9a-f]{64}$' AND
snapshot_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_project_tool_definition_snapshot_json_check CHECK (
jsonb_typeof(snapshot_json) = 'object' AND
octet_length(snapshot_json::text) BETWEEN 2 AND 8388608 AND
snapshot_json @> jsonb_build_object(
'schema', 'qinglong/project-tool-definition-snapshot@v1',
'projectId', project_id,
'activeVectorDigest', active_vector_digest,
'definitionsDigest', definitions_digest,
'snapshotDigest', snapshot_digest
)
),
CONSTRAINT ql3_project_tool_definition_snapshot_time_check CHECK (
committed_at_ms >= 0
)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_project_tool_definition_snapshot_digest_uidx
ON "ql3"."project_tool_definition_snapshots" (snapshot_digest)
`.trim(),
`
CREATE INDEX ql3_project_tool_definition_snapshot_current_idx
ON "ql3"."project_tool_definition_snapshots"
(project_id, committed_at_ms DESC, active_vector_digest)
`.trim(),
`
CREATE TABLE "ql3"."project_tool_definition_snapshot_sources" (
project_id varchar(128) NOT NULL,
active_vector_digest char(64) NOT NULL,
package_name varchar(63) NOT NULL,
installation_id varchar(128) NOT NULL,
generation integer NOT NULL,
generation_digest char(64) NOT NULL,
lock_digest char(64) NOT NULL,
revision_digest char(64) NOT NULL,
CONSTRAINT project_tool_definition_snapshot_sources_pkey
PRIMARY KEY (project_id, active_vector_digest, package_name),
CONSTRAINT ql3_project_tool_definition_snapshot_source_snapshot_fk
FOREIGN KEY (project_id, active_vector_digest)
REFERENCES "ql3"."project_tool_definition_snapshots"
(project_id, active_vector_digest)
ON DELETE RESTRICT,
CONSTRAINT ql3_project_tool_definition_snapshot_source_install_fk
FOREIGN KEY (
project_id, package_name, installation_id, generation, lock_digest
)
REFERENCES "ql3"."plugin_package_installs" (
project_id, package_name, installation_id, target_generation, lock_digest
)
ON DELETE RESTRICT,
CONSTRAINT ql3_project_tool_definition_snapshot_source_revision_fk
FOREIGN KEY (
project_id, package_name, generation, generation_digest,
lock_digest, revision_digest
)
REFERENCES "ql3"."plugin_package_materialized_revisions" (
project_id, package_name, generation, generation_digest,
lock_digest, revision_digest
)
ON DELETE RESTRICT,
CONSTRAINT ql3_project_tool_definition_snapshot_source_identity_check CHECK (
package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$' AND
installation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
generation BETWEEN 1 AND 2147483647
),
CONSTRAINT ql3_project_tool_definition_snapshot_source_digest_check CHECK (
active_vector_digest ~ '^[0-9a-f]{64}$' AND
generation_digest ~ '^[0-9a-f]{64}$' AND
lock_digest ~ '^[0-9a-f]{64}$' AND
revision_digest ~ '^[0-9a-f]{64}$'
)
)
`.trim(),
`
CREATE INDEX ql3_project_tool_definition_snapshot_source_generation_idx
ON "ql3"."project_tool_definition_snapshot_sources"
(generation_digest, project_id, package_name)
`.trim(),
`
CREATE INDEX ql3_project_tool_definition_snapshot_source_install_idx
ON "ql3"."project_tool_definition_snapshot_sources"
(installation_id, active_vector_digest)
`.trim(),
`
REVOKE ALL
ON "ql3"."project_tool_definition_snapshots",
"ql3"."project_tool_definition_snapshot_sources"
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_worker_ingress
`.trim(),
`
GRANT SELECT, INSERT
ON "ql3"."project_tool_definition_snapshots",
"ql3"."project_tool_definition_snapshot_sources"
TO ql3_package_executor
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 26,
migration_id = 'pg-0027-project-tool-definition-snapshots',
capabilities = '${CAPABILITIES_V26}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 25
AND migration_id = 'pg-0026-plugin-package-task-reconciliations'
AND capabilities = '${CAPABILITIES_V25}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 25'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,299 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V26 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V27 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0028StepRunsMigration = definePostgresSqlMigration({
id: 'pg-0028-step-runs',
statements: [
`
DO $ql3$
BEGIN
IF EXISTS (
SELECT 1 FROM "ql3"."run_attempts" WHERE step_run_id IS NOT NULL
UNION ALL
SELECT 1 FROM "ql3"."run_events" WHERE step_run_id IS NOT NULL
) THEN
RAISE EXCEPTION
'legacy step_run_id cannot be upgraded without StepRun authority'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
`
ALTER TABLE "ql3"."run_attempts"
ALTER COLUMN step_run_id TYPE varchar(128)
`.trim(),
`
ALTER TABLE "ql3"."run_events"
ALTER COLUMN step_run_id TYPE varchar(128)
`.trim(),
`
CREATE TABLE "ql3"."step_runs" (
id varchar(128) PRIMARY KEY,
run_id varchar(36) NOT NULL,
parent_step_run_id varchar(128),
step_key varchar(128) NOT NULL,
kind varchar(32) NOT NULL,
definition_ref varchar(512) NOT NULL,
definition_digest char(64) NOT NULL,
required boolean NOT NULL,
status varchar(32) NOT NULL,
version integer NOT NULL,
attempt_count integer NOT NULL,
input_ref varchar(512),
output_ref varchar(512),
approval_request_id varchar(128),
ready_at_ms bigint,
started_at_ms bigint,
finished_at_ms bigint,
result_code varchar(64),
error_summary varchar(2048),
created_at_ms bigint NOT NULL,
updated_at_ms bigint NOT NULL,
last_mutation_id varchar(128) NOT NULL,
step_run_digest char(64) NOT NULL,
step_run_json jsonb NOT NULL,
CONSTRAINT ql3_step_runs_run_fk
FOREIGN KEY (run_id) REFERENCES "ql3"."runs" (id) ON DELETE CASCADE,
CONSTRAINT ql3_step_runs_identity_check CHECK (
id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
(parent_step_run_id IS NULL OR
(parent_step_run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
parent_step_run_id <> id)) AND
step_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
octet_length(definition_ref) BETWEEN 1 AND 512
),
CONSTRAINT ql3_step_runs_kind_check CHECK (
kind IN (
'task', 'tool', 'model', 'agent', 'condition', 'approval',
'subworkflow'
)
),
CONSTRAINT ql3_step_runs_status_check CHECK (
status IN (
'pending', 'ready', 'waiting_approval', 'running', 'lost',
'succeeded', 'failed', 'skipped', 'cancelled', 'timed_out'
)
),
CONSTRAINT ql3_step_runs_digest_check CHECK (
definition_digest ~ '^[0-9a-f]{64}$' AND
step_run_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_step_runs_counter_check CHECK (
version BETWEEN 1 AND 2147483647 AND
attempt_count BETWEEN 0 AND 64
),
CONSTRAINT ql3_step_runs_reference_check CHECK (
(input_ref IS NULL OR octet_length(input_ref) BETWEEN 1 AND 512) AND
(output_ref IS NULL OR octet_length(output_ref) BETWEEN 1 AND 512) AND
(approval_request_id IS NULL OR
approval_request_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$')
),
CONSTRAINT ql3_step_runs_time_check CHECK (
created_at_ms >= 0 AND updated_at_ms >= created_at_ms AND
(ready_at_ms IS NULL OR
ready_at_ms BETWEEN created_at_ms AND updated_at_ms) AND
(started_at_ms IS NULL OR
(ready_at_ms IS NOT NULL AND
started_at_ms BETWEEN ready_at_ms AND updated_at_ms)) AND
(finished_at_ms IS NULL OR
(finished_at_ms BETWEEN created_at_ms AND updated_at_ms AND
(ready_at_ms IS NULL OR finished_at_ms >= ready_at_ms) AND
(started_at_ms IS NULL OR finished_at_ms >= started_at_ms)))
),
CONSTRAINT ql3_step_runs_state_shape_check CHECK (
(status = 'pending' AND ready_at_ms IS NULL AND
started_at_ms IS NULL AND finished_at_ms IS NULL) OR
(status IN ('ready', 'waiting_approval') AND ready_at_ms IS NOT NULL AND
started_at_ms IS NULL AND finished_at_ms IS NULL) OR
(status IN ('running', 'lost') AND ready_at_ms IS NOT NULL AND
started_at_ms IS NOT NULL AND finished_at_ms IS NULL) OR
(status IN ('succeeded', 'failed', 'skipped', 'cancelled', 'timed_out')
AND finished_at_ms IS NOT NULL)
),
CONSTRAINT ql3_step_runs_approval_shape_check CHECK (
status <> 'waiting_approval' OR approval_request_id IS NOT NULL
),
CONSTRAINT ql3_step_runs_result_shape_check CHECK (
(output_ref IS NULL OR status = 'succeeded') AND
((status = 'succeeded' AND result_code IS NULL AND
error_summary IS NULL) OR
(status IN ('failed', 'skipped', 'cancelled', 'timed_out', 'lost')
AND result_code IS NOT NULL) OR
(status IN ('pending', 'ready', 'waiting_approval', 'running')
AND result_code IS NULL AND error_summary IS NULL)) AND
(result_code IS NULL OR result_code ~ '^[a-z][a-z0-9_]{0,63}$') AND
(error_summary IS NULL OR octet_length(error_summary) BETWEEN 1 AND 2048)
),
CONSTRAINT ql3_step_runs_mutation_identity_check CHECK (
last_mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
),
CONSTRAINT ql3_step_runs_json_check CHECK (
jsonb_typeof(step_run_json) = 'object' AND
octet_length(step_run_json::text) BETWEEN 2 AND 16384 AND
step_run_json @> jsonb_build_object(
'schema', 'qinglong/step-run@v1',
'id', id,
'runId', run_id,
'parentStepRunId', parent_step_run_id,
'stepKey', step_key,
'kind', kind,
'definitionRef', definition_ref,
'definitionDigest', definition_digest,
'required', required,
'status', status,
'version', version,
'attemptCount', attempt_count,
'inputRef', input_ref,
'outputRef', output_ref,
'approvalRequestId', approval_request_id,
'readyAtMs', ready_at_ms,
'startedAtMs', started_at_ms,
'finishedAtMs', finished_at_ms,
'resultCode', result_code,
'errorSummary', error_summary,
'createdAtMs', created_at_ms,
'updatedAtMs', updated_at_ms,
'lastMutationId', last_mutation_id,
'stepRunDigest', step_run_digest
)
)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_step_runs_run_id_uidx
ON "ql3"."step_runs" (run_id, id)
`.trim(),
`
ALTER TABLE "ql3"."step_runs"
ADD CONSTRAINT ql3_step_runs_parent_fk
FOREIGN KEY (run_id, parent_step_run_id)
REFERENCES "ql3"."step_runs" (run_id, id)
ON DELETE RESTRICT
`.trim(),
`
CREATE UNIQUE INDEX ql3_step_runs_run_step_uidx
ON "ql3"."step_runs" (run_id, step_key)
`.trim(),
`
CREATE INDEX ql3_step_runs_run_status_idx
ON "ql3"."step_runs" (run_id, status, id)
`.trim(),
`
CREATE INDEX ql3_step_runs_recovery_idx
ON "ql3"."step_runs" (status, updated_at_ms, id)
WHERE status IN ('waiting_approval', 'running', 'lost')
`.trim(),
`
CREATE TABLE "ql3"."step_run_mutations" (
mutation_id varchar(128) PRIMARY KEY,
mutation_digest char(64) NOT NULL,
run_id varchar(36) NOT NULL,
step_run_id varchar(128) NOT NULL,
step_run_digest char(64) NOT NULL,
event_id varchar(36) NOT NULL,
event_sequence integer NOT NULL,
run_version integer NOT NULL,
step_run_json jsonb NOT NULL,
committed_at_ms bigint NOT NULL,
CONSTRAINT ql3_step_run_mutations_step_fk
FOREIGN KEY (run_id, step_run_id)
REFERENCES "ql3"."step_runs" (run_id, id) ON DELETE CASCADE,
CONSTRAINT ql3_step_run_mutations_event_fk
FOREIGN KEY (event_id) REFERENCES "ql3"."run_events" (id)
ON DELETE CASCADE,
CONSTRAINT ql3_step_run_mutations_identity_check CHECK (
mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
step_run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
),
CONSTRAINT ql3_step_run_mutations_digest_check CHECK (
mutation_digest ~ '^[0-9a-f]{64}$' AND
step_run_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_step_run_mutations_counter_check CHECK (
event_sequence BETWEEN 1 AND 2147483647 AND
run_version BETWEEN 1 AND 2147483647 AND
committed_at_ms >= 0
),
CONSTRAINT ql3_step_run_mutations_json_check CHECK (
jsonb_typeof(step_run_json) = 'object' AND
octet_length(step_run_json::text) BETWEEN 2 AND 16384 AND
step_run_json @> jsonb_build_object(
'schema', 'qinglong/step-run@v1',
'id', step_run_id,
'runId', run_id,
'lastMutationId', mutation_id,
'stepRunDigest', step_run_digest
)
)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_step_run_mutations_event_uidx
ON "ql3"."step_run_mutations" (event_id)
`.trim(),
`
CREATE INDEX ql3_step_run_mutations_step_idx
ON "ql3"."step_run_mutations"
(run_id, step_run_id, event_sequence, mutation_id)
`.trim(),
`
ALTER TABLE "ql3"."run_attempts"
ADD CONSTRAINT ql3_run_attempts_step_run_fk
FOREIGN KEY (run_id, step_run_id)
REFERENCES "ql3"."step_runs" (run_id, id)
ON DELETE RESTRICT
`.trim(),
`
ALTER TABLE "ql3"."run_events"
ADD CONSTRAINT ql3_run_events_step_run_fk
FOREIGN KEY (run_id, step_run_id)
REFERENCES "ql3"."step_runs" (run_id, id)
ON DELETE RESTRICT
`.trim(),
`
REVOKE ALL
ON "ql3"."step_runs", "ql3"."step_run_mutations"
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_package_executor, ql3_worker_ingress
`.trim(),
`
GRANT SELECT, INSERT, UPDATE
ON "ql3"."step_runs"
TO ql3_runtime
`.trim(),
`
GRANT SELECT, INSERT
ON "ql3"."step_run_mutations"
TO ql3_runtime
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 27,
migration_id = 'pg-0028-step-runs',
capabilities = '${CAPABILITIES_V27}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 26
AND migration_id = 'pg-0027-project-tool-definition-snapshots'
AND capabilities = '${CAPABILITIES_V26}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 26'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,213 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V27 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V28 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_evidence":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0029ToolExecutionEvidenceMigration =
definePostgresSqlMigration({
id: 'pg-0029-tool-execution-evidence',
statements: [
`
CREATE TABLE "ql3"."tool_execution_trace_anchors" (
trace_id char(32) NOT NULL,
span_id char(16) NOT NULL,
parent_span_id char(16),
project_id varchar(128) NOT NULL,
run_id varchar(36) NOT NULL,
step_run_id varchar(128) NOT NULL,
invocation_plan_digest char(64) NOT NULL,
binding_digest char(64) NOT NULL,
adapter_digest char(64) NOT NULL,
redaction_contract_digest char(64) NOT NULL,
audit_contract_digest char(64) NOT NULL,
created_at_ms bigint NOT NULL,
trace_digest char(64) NOT NULL,
trace_json jsonb NOT NULL,
CONSTRAINT tool_execution_trace_anchors_pkey
PRIMARY KEY (trace_id, span_id),
CONSTRAINT ql3_tool_execution_trace_step_fk
FOREIGN KEY (run_id, step_run_id)
REFERENCES "ql3"."step_runs" (run_id, id) ON DELETE CASCADE,
CONSTRAINT ql3_tool_execution_trace_identity_check CHECK (
trace_id ~ '^[0-9a-f]{32}$' AND
span_id ~ '^[0-9a-f]{16}$' AND
(parent_span_id IS NULL OR (
parent_span_id ~ '^[0-9a-f]{16}$' AND parent_span_id <> span_id
)) AND
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
step_run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
),
CONSTRAINT ql3_tool_execution_trace_digest_check CHECK (
invocation_plan_digest ~ '^[0-9a-f]{64}$' AND
binding_digest ~ '^[0-9a-f]{64}$' AND
adapter_digest ~ '^[0-9a-f]{64}$' AND
redaction_contract_digest ~ '^[0-9a-f]{64}$' AND
audit_contract_digest ~ '^[0-9a-f]{64}$' AND
trace_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_tool_execution_trace_time_check CHECK (
created_at_ms >= 0
),
CONSTRAINT ql3_tool_execution_trace_json_check CHECK (
jsonb_typeof(trace_json) = 'object' AND
octet_length(trace_json::text) BETWEEN 2 AND 16384 AND
trace_json @> jsonb_build_object(
'schema', 'qinglong/tool-execution-trace-anchor@v1',
'traceId', trace_id,
'spanId', span_id,
'parentSpanId', parent_span_id,
'projectId', project_id,
'runId', run_id,
'stepRunId', step_run_id,
'invocationPlanDigest', invocation_plan_digest,
'bindingDigest', binding_digest,
'adapterDigest', adapter_digest,
'redactionContractDigest', redaction_contract_digest,
'auditContractDigest', audit_contract_digest,
'createdAtMs', created_at_ms,
'traceDigest', trace_digest
)
)
)
`.trim(),
`
CREATE INDEX ql3_tool_execution_trace_run_idx
ON "ql3"."tool_execution_trace_anchors"
(run_id, created_at_ms, trace_id, span_id)
`.trim(),
`
CREATE INDEX ql3_tool_execution_trace_step_idx
ON "ql3"."tool_execution_trace_anchors"
(run_id, step_run_id, created_at_ms, trace_id, span_id)
`.trim(),
`
CREATE TABLE "ql3"."tool_execution_audit_receipts" (
event_id uuid PRIMARY KEY,
project_id varchar(128) NOT NULL,
run_id varchar(36) NOT NULL,
step_run_id varchar(128) NOT NULL,
trace_id char(32) NOT NULL,
span_id char(16) NOT NULL,
trace_digest char(64) NOT NULL,
invocation_plan_digest char(64) NOT NULL,
binding_digest char(64) NOT NULL,
audit_record_digest char(64) NOT NULL,
created_at_ms bigint NOT NULL,
receipt_digest char(64) NOT NULL,
audit_json jsonb NOT NULL,
receipt_json jsonb NOT NULL,
CONSTRAINT ql3_tool_execution_audit_event_fk
FOREIGN KEY (event_id)
REFERENCES "ql3"."security_audit_events" (event_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_tool_execution_audit_trace_fk
FOREIGN KEY (trace_id, span_id)
REFERENCES "ql3"."tool_execution_trace_anchors" (trace_id, span_id)
ON DELETE CASCADE,
CONSTRAINT ql3_tool_execution_audit_step_fk
FOREIGN KEY (run_id, step_run_id)
REFERENCES "ql3"."step_runs" (run_id, id) ON DELETE CASCADE,
CONSTRAINT ql3_tool_execution_audit_identity_check CHECK (
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
step_run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
trace_id ~ '^[0-9a-f]{32}$' AND
span_id ~ '^[0-9a-f]{16}$'
),
CONSTRAINT ql3_tool_execution_audit_digest_check CHECK (
trace_digest ~ '^[0-9a-f]{64}$' AND
invocation_plan_digest ~ '^[0-9a-f]{64}$' AND
binding_digest ~ '^[0-9a-f]{64}$' AND
audit_record_digest ~ '^[0-9a-f]{64}$' AND
receipt_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_tool_execution_audit_time_check CHECK (
created_at_ms >= 0
),
CONSTRAINT ql3_tool_execution_audit_json_check CHECK (
jsonb_typeof(audit_json) = 'object' AND
octet_length(audit_json::text) BETWEEN 2 AND 8192 AND
audit_json @> jsonb_build_object(
'eventId', event_id,
'projectId', project_id,
'operationId', 'tool.invoke.start',
'outcome', 'allowed',
'occurredAtMs', created_at_ms
) AND
jsonb_typeof(audit_json -> 'fence') = 'object'
),
CONSTRAINT ql3_tool_execution_audit_receipt_json_check CHECK (
jsonb_typeof(receipt_json) = 'object' AND
octet_length(receipt_json::text) BETWEEN 2 AND 16384 AND
receipt_json @> jsonb_build_object(
'schema', 'qinglong/tool-execution-audit-receipt@v1',
'eventId', event_id,
'projectId', project_id,
'runId', run_id,
'stepRunId', step_run_id,
'traceId', trace_id,
'spanId', span_id,
'traceDigest', trace_digest,
'invocationPlanDigest', invocation_plan_digest,
'bindingDigest', binding_digest,
'auditRecordDigest', audit_record_digest,
'createdAtMs', created_at_ms,
'receiptDigest', receipt_digest
)
)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_tool_execution_audit_trace_uidx
ON "ql3"."tool_execution_audit_receipts" (trace_id, span_id)
`.trim(),
`
CREATE INDEX ql3_tool_execution_audit_run_idx
ON "ql3"."tool_execution_audit_receipts"
(run_id, created_at_ms, trace_id, span_id)
`.trim(),
`
CREATE INDEX ql3_tool_execution_audit_step_idx
ON "ql3"."tool_execution_audit_receipts"
(run_id, step_run_id, created_at_ms, event_id)
`.trim(),
`
REVOKE ALL
ON "ql3"."tool_execution_trace_anchors",
"ql3"."tool_execution_audit_receipts"
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_package_executor, ql3_worker_ingress
`.trim(),
`
GRANT SELECT, INSERT
ON "ql3"."tool_execution_trace_anchors",
"ql3"."tool_execution_audit_receipts"
TO ql3_runtime
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 28,
migration_id = 'pg-0029-tool-execution-evidence',
capabilities = '${CAPABILITIES_V28}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 27
AND migration_id = 'pg-0028-step-runs'
AND capabilities = '${CAPABILITIES_V27}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 27'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,145 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V28 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_evidence":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V29 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0030ToolExecutionStartBarriersMigration =
definePostgresSqlMigration({
id: 'pg-0030-tool-execution-start-barriers',
statements: [
`
CREATE TABLE "ql3"."tool_execution_start_barriers" (
start_id varchar(128) PRIMARY KEY,
project_id varchar(128) NOT NULL,
run_id varchar(36) NOT NULL,
step_run_id varchar(128) NOT NULL,
started_step_run_version integer NOT NULL,
step_run_mutation_id varchar(128) NOT NULL,
run_event_id varchar(128) NOT NULL,
trace_id char(32) NOT NULL,
span_id char(16) NOT NULL,
audit_event_id uuid NOT NULL,
command_digest char(64) NOT NULL,
barrier_digest char(64) NOT NULL,
started_at_ms bigint NOT NULL,
barrier_json jsonb NOT NULL,
CONSTRAINT ql3_tool_start_step_fk
FOREIGN KEY (run_id, step_run_id)
REFERENCES "ql3"."step_runs" (run_id, id) ON DELETE RESTRICT,
CONSTRAINT ql3_tool_start_mutation_fk
FOREIGN KEY (step_run_mutation_id)
REFERENCES "ql3"."step_run_mutations" (mutation_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_tool_start_event_fk
FOREIGN KEY (run_event_id)
REFERENCES "ql3"."run_events" (id) ON DELETE RESTRICT,
CONSTRAINT ql3_tool_start_trace_fk
FOREIGN KEY (trace_id, span_id)
REFERENCES "ql3"."tool_execution_trace_anchors" (trace_id, span_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_tool_start_audit_fk
FOREIGN KEY (audit_event_id)
REFERENCES "ql3"."tool_execution_audit_receipts" (event_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_tool_start_identity_check CHECK (
start_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
step_run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
step_run_mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
run_event_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
trace_id ~ '^[0-9a-f]{32}$' AND span_id ~ '^[0-9a-f]{16}$'
),
CONSTRAINT ql3_tool_start_version_time_check CHECK (
started_step_run_version BETWEEN 2 AND 2147483647 AND
started_at_ms >= 0
),
CONSTRAINT ql3_tool_start_digest_check CHECK (
command_digest ~ '^[0-9a-f]{64}$' AND
barrier_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_tool_start_json_check CHECK (
jsonb_typeof(barrier_json) = 'object' AND
octet_length(barrier_json::text) BETWEEN 2 AND 16384 AND
barrier_json @> jsonb_build_object(
'schema', 'qinglong/tool-execution-start-barrier@v1',
'startId', start_id,
'projectId', project_id,
'runId', run_id,
'stepRunId', step_run_id,
'startedStepRunVersion', started_step_run_version,
'stepRunMutationId', step_run_mutation_id,
'runEventId', run_event_id,
'traceId', trace_id,
'spanId', span_id,
'auditEventId', audit_event_id,
'commandDigest', command_digest,
'barrierDigest', barrier_digest,
'startedAtMs', started_at_ms
)
)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_tool_start_step_version_uidx
ON "ql3"."tool_execution_start_barriers"
(run_id, step_run_id, started_step_run_version)
`.trim(),
`
CREATE UNIQUE INDEX ql3_tool_start_mutation_uidx
ON "ql3"."tool_execution_start_barriers" (step_run_mutation_id)
`.trim(),
`
CREATE UNIQUE INDEX ql3_tool_start_event_uidx
ON "ql3"."tool_execution_start_barriers" (run_event_id)
`.trim(),
`
CREATE UNIQUE INDEX ql3_tool_start_trace_uidx
ON "ql3"."tool_execution_start_barriers" (trace_id, span_id)
`.trim(),
`
CREATE UNIQUE INDEX ql3_tool_start_audit_uidx
ON "ql3"."tool_execution_start_barriers" (audit_event_id)
`.trim(),
`
CREATE INDEX ql3_tool_start_run_time_idx
ON "ql3"."tool_execution_start_barriers"
(run_id, started_at_ms, start_id)
`.trim(),
`
REVOKE ALL
ON "ql3"."tool_execution_start_barriers"
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_package_executor, ql3_worker_ingress
`.trim(),
`
GRANT SELECT, INSERT
ON "ql3"."tool_execution_start_barriers"
TO ql3_runtime
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 29,
migration_id = 'pg-0030-tool-execution-start-barriers',
capabilities = '${CAPABILITIES_V29}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 28
AND migration_id = 'pg-0029-tool-execution-evidence'
AND capabilities = '${CAPABILITIES_V28}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 28'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,167 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V29 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V30 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0031ToolInvocationArtifactsMigration =
definePostgresSqlMigration({
id: 'pg-0031-tool-invocation-artifacts',
statements: [
`
CREATE TABLE "ql3"."tool_invocation_input_artifacts" (
artifact_id varchar(128) PRIMARY KEY,
project_id varchar(128) NOT NULL,
action_ref varchar(255) NOT NULL,
input_digest char(64) NOT NULL,
invocation_action_digest char(64) NOT NULL,
artifact_digest char(64) NOT NULL,
key_id varchar(128) NOT NULL,
algorithm varchar(32) NOT NULL,
plaintext_bytes integer NOT NULL,
sealed_at_ms bigint NOT NULL,
artifact_json jsonb NOT NULL,
CONSTRAINT ql3_tool_input_artifact_project_fk
FOREIGN KEY (project_id)
REFERENCES "ql3"."projects" (id) ON DELETE RESTRICT,
CONSTRAINT ql3_tool_input_artifact_identity_check CHECK (
artifact_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
action_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$' AND
key_id ~ '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$' AND
algorithm = 'aes-256-gcm'
),
CONSTRAINT ql3_tool_input_artifact_digest_check CHECK (
input_digest ~ '^[0-9a-f]{64}$' AND
invocation_action_digest ~ '^[0-9a-f]{64}$' AND
artifact_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_tool_input_artifact_budget_check CHECK (
plaintext_bytes BETWEEN 0 AND 65536 AND sealed_at_ms >= 0
),
CONSTRAINT ql3_tool_input_artifact_json_check CHECK (
jsonb_typeof(artifact_json) = 'object' AND
octet_length(artifact_json::text) BETWEEN 2 AND 98304 AND
artifact_json @> jsonb_build_object(
'schema', 'qinglong/tool-invocation-input-artifact@v1',
'artifactId', artifact_id,
'projectId', project_id,
'actionRef', action_ref,
'inputDigest', input_digest,
'invocationActionDigest', invocation_action_digest,
'artifactDigest', artifact_digest,
'keyId', key_id,
'algorithm', algorithm,
'plaintextBytes', plaintext_bytes,
'sealedAtMs', sealed_at_ms
)
)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_tool_input_artifact_action_uidx
ON "ql3"."tool_invocation_input_artifacts" (project_id, action_ref)
`.trim(),
`
CREATE INDEX ql3_tool_input_artifact_project_time_idx
ON "ql3"."tool_invocation_input_artifacts"
(project_id, sealed_at_ms, artifact_id)
`.trim(),
`
CREATE TABLE "ql3"."tool_invocation_preview_artifacts" (
artifact_id varchar(128) PRIMARY KEY,
project_id varchar(128) NOT NULL,
action_ref varchar(255) NOT NULL,
action_digest char(64) NOT NULL,
preview_digest char(64) NOT NULL,
redaction_contract_digest char(64) NOT NULL,
artifact_digest char(64) NOT NULL,
byte_length integer NOT NULL,
sealed_at_ms bigint NOT NULL,
artifact_json jsonb NOT NULL,
CONSTRAINT ql3_tool_preview_artifact_project_fk
FOREIGN KEY (project_id)
REFERENCES "ql3"."projects" (id) ON DELETE RESTRICT,
CONSTRAINT ql3_tool_preview_artifact_identity_check CHECK (
artifact_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
action_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$'
),
CONSTRAINT ql3_tool_preview_artifact_digest_check CHECK (
action_digest ~ '^[0-9a-f]{64}$' AND
preview_digest ~ '^[0-9a-f]{64}$' AND
redaction_contract_digest ~ '^[0-9a-f]{64}$' AND
artifact_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_tool_preview_artifact_budget_check CHECK (
byte_length BETWEEN 2 AND 8192 AND sealed_at_ms >= 0
),
CONSTRAINT ql3_tool_preview_artifact_json_check CHECK (
jsonb_typeof(artifact_json) = 'object' AND
octet_length(artifact_json::text) BETWEEN 2 AND 16384 AND
artifact_json @> jsonb_build_object(
'schema', 'qinglong/tool-invocation-preview-artifact@v1',
'artifactId', artifact_id,
'projectId', project_id,
'actionRef', action_ref,
'actionDigest', action_digest,
'previewDigest', preview_digest,
'redactionContractDigest', redaction_contract_digest,
'artifactDigest', artifact_digest,
'byteLength', byte_length,
'sealedAtMs', sealed_at_ms
)
)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_tool_preview_artifact_action_uidx
ON "ql3"."tool_invocation_preview_artifacts" (project_id, action_ref)
`.trim(),
`
CREATE UNIQUE INDEX ql3_tool_preview_artifact_action_digest_uidx
ON "ql3"."tool_invocation_preview_artifacts" (action_digest)
`.trim(),
`
CREATE INDEX ql3_tool_preview_artifact_project_time_idx
ON "ql3"."tool_invocation_preview_artifacts"
(project_id, sealed_at_ms, artifact_id)
`.trim(),
`
REVOKE ALL
ON "ql3"."tool_invocation_input_artifacts",
"ql3"."tool_invocation_preview_artifacts"
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_package_executor, ql3_worker_ingress
`.trim(),
`
GRANT SELECT, INSERT
ON "ql3"."tool_invocation_input_artifacts",
"ql3"."tool_invocation_preview_artifacts"
TO ql3_runtime
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 30,
migration_id = 'pg-0031-tool-invocation-artifacts',
capabilities = '${CAPABILITIES_V30}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 29
AND migration_id = 'pg-0030-tool-execution-start-barriers'
AND capabilities = '${CAPABILITIES_V29}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 29'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,122 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V30 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V31 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0032ToolExecutionArtifactBindingsMigration =
definePostgresSqlMigration({
id: 'pg-0032-tool-execution-artifact-bindings',
statements: [
`
CREATE UNIQUE INDEX ql3_tool_input_artifact_start_binding_uidx
ON "ql3"."tool_invocation_input_artifacts" (
artifact_id, artifact_digest, project_id, action_ref, input_digest
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_tool_preview_artifact_start_binding_uidx
ON "ql3"."tool_invocation_preview_artifacts" (
artifact_id, artifact_digest, project_id, action_ref, action_digest,
preview_digest, redaction_contract_digest
)
`.trim(),
`
CREATE TABLE "ql3"."tool_execution_start_artifact_bindings" (
start_id varchar(128) PRIMARY KEY,
project_id varchar(128) NOT NULL,
action_ref varchar(255) NOT NULL,
input_artifact_id varchar(128) NOT NULL,
input_artifact_digest char(64) NOT NULL,
input_digest char(64) NOT NULL,
preview_artifact_id varchar(128) NOT NULL,
preview_artifact_digest char(64) NOT NULL,
action_digest char(64) NOT NULL,
preview_digest char(64) NOT NULL,
redaction_contract_digest char(64) NOT NULL,
bound_at_ms bigint NOT NULL,
CONSTRAINT ql3_tool_start_artifact_barrier_fk
FOREIGN KEY (start_id)
REFERENCES "ql3"."tool_execution_start_barriers" (start_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_tool_start_input_artifact_fk
FOREIGN KEY (
input_artifact_id, input_artifact_digest, project_id, action_ref,
input_digest
)
REFERENCES "ql3"."tool_invocation_input_artifacts" (
artifact_id, artifact_digest, project_id, action_ref, input_digest
) ON DELETE RESTRICT,
CONSTRAINT ql3_tool_start_preview_artifact_fk
FOREIGN KEY (
preview_artifact_id, preview_artifact_digest, project_id, action_ref,
action_digest, preview_digest, redaction_contract_digest
)
REFERENCES "ql3"."tool_invocation_preview_artifacts" (
artifact_id, artifact_digest, project_id, action_ref, action_digest,
preview_digest, redaction_contract_digest
) ON DELETE RESTRICT,
CONSTRAINT ql3_tool_start_artifact_identity_check CHECK (
start_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
action_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$' AND
input_artifact_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
preview_artifact_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
),
CONSTRAINT ql3_tool_start_artifact_digest_check CHECK (
input_artifact_digest ~ '^[0-9a-f]{64}$' AND
input_digest ~ '^[0-9a-f]{64}$' AND
preview_artifact_digest ~ '^[0-9a-f]{64}$' AND
action_digest ~ '^[0-9a-f]{64}$' AND
preview_digest ~ '^[0-9a-f]{64}$' AND
redaction_contract_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_tool_start_artifact_time_check CHECK (bound_at_ms >= 0)
)
`.trim(),
`
CREATE INDEX ql3_tool_start_artifact_input_idx
ON "ql3"."tool_execution_start_artifact_bindings"
(input_artifact_id, start_id)
`.trim(),
`
CREATE INDEX ql3_tool_start_artifact_preview_idx
ON "ql3"."tool_execution_start_artifact_bindings"
(preview_artifact_id, start_id)
`.trim(),
`
REVOKE ALL
ON "ql3"."tool_execution_start_artifact_bindings"
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_package_executor, ql3_worker_ingress
`.trim(),
`
GRANT SELECT, INSERT
ON "ql3"."tool_execution_start_artifact_bindings"
TO ql3_runtime
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 31,
migration_id = 'pg-0032-tool-execution-artifact-bindings',
capabilities = '${CAPABILITIES_V31}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 30
AND migration_id = 'pg-0031-tool-invocation-artifacts'
AND capabilities = '${CAPABILITIES_V30}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 30'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,187 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V31 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V32 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0033ToolExecutionCompletionsMigration =
definePostgresSqlMigration({
id: 'pg-0033-tool-execution-completions',
statements: [
`
CREATE TABLE "ql3"."tool_execution_completions" (
start_id varchar(128) PRIMARY KEY,
artifact_id varchar(128) NOT NULL,
project_id varchar(128) NOT NULL,
run_id varchar(128) NOT NULL,
step_run_id varchar(128) NOT NULL,
started_step_run_version integer NOT NULL,
completed_step_run_version integer NOT NULL,
barrier_digest char(64) NOT NULL,
adapter_digest char(64) NOT NULL,
output_digest char(64) NOT NULL,
execution_result_digest char(64) NOT NULL,
artifact_digest char(64) NOT NULL,
key_id varchar(128) NOT NULL,
algorithm varchar(32) NOT NULL,
plaintext_bytes integer NOT NULL,
step_run_mutation_id varchar(128) NOT NULL,
step_run_mutation_digest char(64) NOT NULL,
completed_step_run_digest char(64) NOT NULL,
run_event_id varchar(128) NOT NULL,
completed_at_ms bigint NOT NULL,
completion_digest char(64) NOT NULL,
artifact_json jsonb NOT NULL,
completion_json jsonb NOT NULL,
CONSTRAINT ql3_tool_completion_start_fk
FOREIGN KEY (start_id)
REFERENCES "ql3"."tool_execution_start_barriers" (start_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_tool_completion_step_fk
FOREIGN KEY (run_id, step_run_id)
REFERENCES "ql3"."step_runs" (run_id, id)
ON DELETE RESTRICT,
CONSTRAINT ql3_tool_completion_mutation_fk
FOREIGN KEY (step_run_mutation_id)
REFERENCES "ql3"."step_run_mutations" (mutation_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_tool_completion_event_fk
FOREIGN KEY (run_event_id)
REFERENCES "ql3"."run_events" (id)
ON DELETE RESTRICT,
CONSTRAINT ql3_tool_completion_identity_check CHECK (
start_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
artifact_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
step_run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
key_id ~ '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$' AND
step_run_mutation_id ~
'^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
run_event_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
),
CONSTRAINT ql3_tool_completion_version_check CHECK (
started_step_run_version BETWEEN 2 AND 2147483646 AND
completed_step_run_version = started_step_run_version + 1
),
CONSTRAINT ql3_tool_completion_digest_check CHECK (
barrier_digest ~ '^[0-9a-f]{64}$' AND
adapter_digest ~ '^[0-9a-f]{64}$' AND
output_digest ~ '^[0-9a-f]{64}$' AND
execution_result_digest ~ '^[0-9a-f]{64}$' AND
artifact_digest ~ '^[0-9a-f]{64}$' AND
step_run_mutation_digest ~ '^[0-9a-f]{64}$' AND
completed_step_run_digest ~ '^[0-9a-f]{64}$' AND
completion_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_tool_completion_budget_check CHECK (
algorithm = 'aes-256-gcm' AND
plaintext_bytes BETWEEN 0 AND 262144 AND
completed_at_ms >= 0 AND
octet_length(artifact_json::text) BETWEEN 2 AND 393216 AND
octet_length(completion_json::text) BETWEEN 2 AND 24576
),
CONSTRAINT ql3_tool_completion_json_check CHECK (
jsonb_typeof(artifact_json) = 'object' AND
artifact_json @> jsonb_build_object(
'schema', 'qinglong/tool-execution-result-artifact@v1',
'artifactId', artifact_id,
'projectId', project_id,
'startId', start_id,
'runId', run_id,
'stepRunId', step_run_id,
'barrierDigest', barrier_digest,
'adapterDigest', adapter_digest,
'outputDigest', output_digest,
'executionResultDigest', execution_result_digest,
'artifactDigest', artifact_digest,
'keyId', key_id,
'algorithm', algorithm,
'plaintextBytes', plaintext_bytes,
'sealedAtMs', completed_at_ms
) AND
jsonb_typeof(completion_json) = 'object' AND
completion_json @> jsonb_build_object(
'schema', 'qinglong/tool-execution-completion@v1',
'startId', start_id,
'projectId', project_id,
'runId', run_id,
'stepRunId', step_run_id,
'startedStepRunVersion', started_step_run_version,
'completedStepRunVersion', completed_step_run_version,
'barrierDigest', barrier_digest,
'adapterDigest', adapter_digest,
'resultArtifact', jsonb_build_object(
'artifactId', artifact_id,
'artifactDigest', artifact_digest,
'outputDigest', output_digest,
'executionResultDigest', execution_result_digest
),
'stepRunMutationId', step_run_mutation_id,
'stepRunMutationDigest', step_run_mutation_digest,
'completedStepRunDigest', completed_step_run_digest,
'runEventId', run_event_id,
'completedAtMs', completed_at_ms,
'completionDigest', completion_digest
)
)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_tool_completion_artifact_uidx
ON "ql3"."tool_execution_completions" (artifact_id)
`.trim(),
`
CREATE UNIQUE INDEX ql3_tool_completion_mutation_uidx
ON "ql3"."tool_execution_completions" (step_run_mutation_id)
`.trim(),
`
CREATE UNIQUE INDEX ql3_tool_completion_event_uidx
ON "ql3"."tool_execution_completions" (run_event_id)
`.trim(),
`
CREATE UNIQUE INDEX ql3_tool_completion_step_version_uidx
ON "ql3"."tool_execution_completions"
(run_id, step_run_id, completed_step_run_version)
`.trim(),
`
CREATE INDEX ql3_tool_completion_project_time_idx
ON "ql3"."tool_execution_completions"
(project_id, completed_at_ms, start_id)
`.trim(),
`
REVOKE ALL
ON "ql3"."tool_execution_completions"
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_package_executor, ql3_worker_ingress
`.trim(),
`
GRANT SELECT, INSERT
ON "ql3"."tool_execution_completions"
TO ql3_runtime
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 32,
migration_id = 'pg-0033-tool-execution-completions',
capabilities = '${CAPABILITIES_V32}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 31
AND migration_id = 'pg-0032-tool-execution-artifact-bindings'
AND capabilities = '${CAPABILITIES_V31}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 31'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,160 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V32 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V33 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0034ToolExecutionFailureCompletionsMigration =
definePostgresSqlMigration({
id: 'pg-0034-tool-execution-failure-completions',
statements: [
`
CREATE TABLE "ql3"."tool_execution_failure_completions" (
start_id varchar(128) PRIMARY KEY,
project_id varchar(128) NOT NULL,
run_id varchar(128) NOT NULL,
step_run_id varchar(128) NOT NULL,
started_step_run_version integer NOT NULL,
completed_step_run_version integer NOT NULL,
barrier_digest char(64) NOT NULL,
adapter_digest char(64) NOT NULL,
outcome varchar(16) NOT NULL,
result_code varchar(64) NOT NULL,
error_summary varchar(128) NOT NULL,
step_run_mutation_id varchar(128) NOT NULL,
step_run_mutation_digest char(64) NOT NULL,
completed_step_run_digest char(64) NOT NULL,
run_event_id varchar(128) NOT NULL,
completed_at_ms bigint NOT NULL,
completion_digest char(64) NOT NULL,
completion_json jsonb NOT NULL,
CONSTRAINT ql3_tool_failure_completion_start_fk
FOREIGN KEY (start_id)
REFERENCES "ql3"."tool_execution_start_barriers" (start_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_tool_failure_completion_step_fk
FOREIGN KEY (run_id, step_run_id)
REFERENCES "ql3"."step_runs" (run_id, id)
ON DELETE RESTRICT,
CONSTRAINT ql3_tool_failure_completion_mutation_fk
FOREIGN KEY (step_run_mutation_id)
REFERENCES "ql3"."step_run_mutations" (mutation_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_tool_failure_completion_event_fk
FOREIGN KEY (run_event_id)
REFERENCES "ql3"."run_events" (id)
ON DELETE RESTRICT,
CONSTRAINT ql3_tool_failure_completion_identity_check CHECK (
start_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
step_run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
step_run_mutation_id ~
'^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
run_event_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
),
CONSTRAINT ql3_tool_failure_completion_version_check CHECK (
started_step_run_version BETWEEN 2 AND 2147483646 AND
completed_step_run_version = started_step_run_version + 1
),
CONSTRAINT ql3_tool_failure_completion_digest_check CHECK (
barrier_digest ~ '^[0-9a-f]{64}$' AND
adapter_digest ~ '^[0-9a-f]{64}$' AND
step_run_mutation_digest ~ '^[0-9a-f]{64}$' AND
completed_step_run_digest ~ '^[0-9a-f]{64}$' AND
completion_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_tool_failure_completion_fact_check CHECK (
(
outcome = 'failed' AND
result_code = 'tool_adapter_failed' AND
error_summary = 'Trusted Tool execution failed'
) OR (
outcome = 'timed_out' AND
result_code = 'tool_deadline_exceeded' AND
error_summary = 'Trusted Tool execution deadline exceeded'
)
),
CONSTRAINT ql3_tool_failure_completion_budget_check CHECK (
completed_at_ms >= 0 AND
octet_length(completion_json::text) BETWEEN 2 AND 24576
),
CONSTRAINT ql3_tool_failure_completion_json_check CHECK (
jsonb_typeof(completion_json) = 'object' AND
completion_json @> jsonb_build_object(
'schema', 'qinglong/tool-execution-failure-completion@v1',
'startId', start_id,
'projectId', project_id,
'runId', run_id,
'stepRunId', step_run_id,
'startedStepRunVersion', started_step_run_version,
'completedStepRunVersion', completed_step_run_version,
'barrierDigest', barrier_digest,
'adapterDigest', adapter_digest,
'outcome', outcome,
'resultCode', result_code,
'errorSummary', error_summary,
'stepRunMutationId', step_run_mutation_id,
'stepRunMutationDigest', step_run_mutation_digest,
'completedStepRunDigest', completed_step_run_digest,
'runEventId', run_event_id,
'completedAtMs', completed_at_ms,
'completionDigest', completion_digest
)
)
)
`.trim(),
`
CREATE UNIQUE INDEX ql3_tool_failure_completion_mutation_uidx
ON "ql3"."tool_execution_failure_completions" (step_run_mutation_id)
`.trim(),
`
CREATE UNIQUE INDEX ql3_tool_failure_completion_event_uidx
ON "ql3"."tool_execution_failure_completions" (run_event_id)
`.trim(),
`
CREATE UNIQUE INDEX ql3_tool_failure_completion_step_version_uidx
ON "ql3"."tool_execution_failure_completions"
(run_id, step_run_id, completed_step_run_version)
`.trim(),
`
CREATE INDEX ql3_tool_failure_completion_project_time_idx
ON "ql3"."tool_execution_failure_completions"
(project_id, completed_at_ms, start_id)
`.trim(),
`
REVOKE ALL
ON "ql3"."tool_execution_failure_completions"
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_package_executor, ql3_worker_ingress
`.trim(),
`
GRANT SELECT, INSERT
ON "ql3"."tool_execution_failure_completions"
TO ql3_runtime
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 33,
migration_id = 'pg-0034-tool-execution-failure-completions',
capabilities = '${CAPABILITIES_V33}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 32
AND migration_id = 'pg-0033-tool-execution-completions'
AND capabilities = '${CAPABILITIES_V32}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 32'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,195 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V33 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V34 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0035ToolResultKeyCatalogMigration = definePostgresSqlMigration({
id: 'pg-0035-tool-result-key-catalog',
statements: [
`
CREATE TABLE "ql3"."tool_result_key_catalog_generations" (
authority varchar(64) NOT NULL,
generation integer NOT NULL,
previous_generation integer,
previous_catalog_digest char(64),
active_key_id varchar(128),
mutation_kind varchar(16) NOT NULL,
mutation_id varchar(128) NOT NULL,
catalog_digest char(64) NOT NULL,
command_digest char(64) NOT NULL,
committed_at_ms bigint NOT NULL,
catalog_json jsonb NOT NULL,
PRIMARY KEY (authority, generation),
CONSTRAINT ql3_tool_result_key_catalog_generation_digest_key
UNIQUE (authority, generation, catalog_digest),
CONSTRAINT ql3_tool_result_key_catalog_mutation_key
UNIQUE (mutation_id),
CONSTRAINT ql3_tool_result_key_catalog_digest_key
UNIQUE (catalog_digest),
CONSTRAINT ql3_tool_result_key_catalog_previous_fk
FOREIGN KEY (
authority, previous_generation, previous_catalog_digest
)
REFERENCES "ql3"."tool_result_key_catalog_generations" (
authority, generation, catalog_digest
) ON DELETE RESTRICT,
CONSTRAINT ql3_tool_result_key_catalog_authority_check CHECK (
authority = 'trusted-tool-results'
),
CONSTRAINT ql3_tool_result_key_catalog_generation_check CHECK (
generation BETWEEN 1 AND 2147483647 AND
(
(
generation = 1 AND
previous_generation IS NULL AND
previous_catalog_digest IS NULL AND
mutation_kind = 'bootstrap'
) OR (
generation > 1 AND
previous_generation = generation - 1 AND
previous_catalog_digest IS NOT NULL AND
mutation_kind IN (
'rotate', 'retire', 'mark_lost', 'restore'
)
)
)
),
CONSTRAINT ql3_tool_result_key_catalog_identity_check CHECK (
active_key_id IS NULL OR
active_key_id ~ '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$'
),
CONSTRAINT ql3_tool_result_key_catalog_digest_check CHECK (
(
previous_catalog_digest IS NULL OR
previous_catalog_digest ~ '^[0-9a-f]{64}$'
) AND
catalog_digest ~ '^[0-9a-f]{64}$' AND
command_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_tool_result_key_catalog_budget_check CHECK (
committed_at_ms >= 0 AND
mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
octet_length(catalog_json::text) BETWEEN 2 AND 65536
),
CONSTRAINT ql3_tool_result_key_catalog_json_check CHECK (
jsonb_typeof(catalog_json) = 'object' AND
catalog_json @> jsonb_build_object(
'schema', 'qinglong/tool-result-key-catalog@v1',
'generation', generation,
'previousCatalogDigest', previous_catalog_digest,
'activeKeyId', active_key_id,
'mutationKind', mutation_kind,
'mutationId', mutation_id,
'catalogDigest', catalog_digest,
'committedAtMs', committed_at_ms
) AND
jsonb_typeof(catalog_json -> 'keys') = 'array' AND
jsonb_array_length(catalog_json -> 'keys') BETWEEN 1 AND 64
)
)
`.trim(),
`
CREATE INDEX ql3_tool_result_key_catalog_current_idx
ON "ql3"."tool_result_key_catalog_generations"
(authority, generation DESC)
`.trim(),
`
CREATE TABLE "ql3"."tool_execution_result_key_bindings" (
start_id varchar(128) PRIMARY KEY,
artifact_id varchar(128) NOT NULL,
artifact_digest char(64) NOT NULL,
catalog_authority varchar(64) NOT NULL,
catalog_generation integer NOT NULL,
catalog_digest char(64) NOT NULL,
key_id varchar(128) NOT NULL,
material_proof char(64) NOT NULL,
binding_digest char(64) NOT NULL,
CONSTRAINT ql3_tool_result_key_binding_artifact_key
UNIQUE (artifact_id),
CONSTRAINT ql3_tool_result_key_binding_digest_key
UNIQUE (binding_digest),
CONSTRAINT ql3_tool_result_key_binding_completion_fk
FOREIGN KEY (start_id)
REFERENCES "ql3"."tool_execution_completions" (start_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_tool_result_key_binding_artifact_fk
FOREIGN KEY (artifact_id)
REFERENCES "ql3"."tool_execution_completions" (artifact_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_tool_result_key_binding_catalog_fk
FOREIGN KEY (
catalog_authority, catalog_generation, catalog_digest
)
REFERENCES "ql3"."tool_result_key_catalog_generations" (
authority, generation, catalog_digest
) ON DELETE RESTRICT,
CONSTRAINT ql3_tool_result_key_binding_authority_check CHECK (
catalog_authority = 'trusted-tool-results'
),
CONSTRAINT ql3_tool_result_key_binding_identity_check CHECK (
start_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
artifact_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
key_id ~ '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$' AND
catalog_generation BETWEEN 1 AND 2147483647
),
CONSTRAINT ql3_tool_result_key_binding_digest_check CHECK (
artifact_digest ~ '^[0-9a-f]{64}$' AND
catalog_digest ~ '^[0-9a-f]{64}$' AND
material_proof ~ '^[0-9a-f]{64}$' AND
binding_digest ~ '^[0-9a-f]{64}$'
)
)
`.trim(),
`
CREATE INDEX ql3_tool_result_key_binding_catalog_idx
ON "ql3"."tool_execution_result_key_bindings"
(catalog_generation, key_id, start_id)
`.trim(),
`
REVOKE ALL
ON "ql3"."tool_result_key_catalog_generations",
"ql3"."tool_execution_result_key_bindings"
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_package_executor, ql3_worker_ingress
`.trim(),
`
GRANT SELECT
ON "ql3"."tool_result_key_catalog_generations"
TO ql3_runtime
`.trim(),
`
GRANT SELECT, INSERT
ON "ql3"."tool_result_key_catalog_generations"
TO ql3_admin
`.trim(),
`
GRANT SELECT, INSERT
ON "ql3"."tool_execution_result_key_bindings"
TO ql3_runtime
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 34,
migration_id = 'pg-0035-tool-result-key-catalog',
capabilities = '${CAPABILITIES_V34}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 33
AND migration_id = 'pg-0034-tool-execution-failure-completions'
AND capabilities = '${CAPABILITIES_V33}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 33'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,234 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V34 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V35 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0036ToolResultRekeyOverlaysMigration =
definePostgresSqlMigration({
id: 'pg-0036-tool-result-rekey-overlays',
statements: [
`
CREATE TABLE "ql3"."tool_execution_result_rekey_overlays" (
overlay_id varchar(128) PRIMARY KEY,
artifact_id varchar(128) NOT NULL,
source_binding_digest char(64) NOT NULL,
revision integer NOT NULL,
previous_overlay_digest char(64),
from_key_id varchar(128) NOT NULL,
target_catalog_authority varchar(64) NOT NULL,
target_catalog_generation integer NOT NULL,
target_catalog_digest char(64) NOT NULL,
target_key_id varchar(128) NOT NULL,
target_material_proof char(64) NOT NULL,
mutation_id varchar(128) NOT NULL,
command_digest char(64) NOT NULL,
overlay_digest char(64) NOT NULL,
rekeyed_at_ms bigint NOT NULL,
overlay_json jsonb NOT NULL,
CONSTRAINT ql3_result_rekey_digest_key UNIQUE (overlay_digest),
CONSTRAINT ql3_result_rekey_artifact_fk FOREIGN KEY (artifact_id)
REFERENCES "ql3"."tool_execution_result_key_bindings" (artifact_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_result_rekey_binding_fk FOREIGN KEY (source_binding_digest)
REFERENCES "ql3"."tool_execution_result_key_bindings" (binding_digest)
ON DELETE RESTRICT,
CONSTRAINT ql3_result_rekey_previous_fk FOREIGN KEY (previous_overlay_digest)
REFERENCES "ql3"."tool_execution_result_rekey_overlays" (overlay_digest)
ON DELETE RESTRICT,
CONSTRAINT ql3_result_rekey_catalog_fk FOREIGN KEY (
target_catalog_authority, target_catalog_generation,
target_catalog_digest
) REFERENCES "ql3"."tool_result_key_catalog_generations" (
authority, generation, catalog_digest
) ON DELETE RESTRICT,
CONSTRAINT ql3_result_rekey_revision_check CHECK (
revision BETWEEN 1 AND 2147483647 AND
((revision = 1 AND previous_overlay_digest IS NULL) OR
(revision > 1 AND previous_overlay_digest IS NOT NULL))
),
CONSTRAINT ql3_result_rekey_authority_check CHECK (
target_catalog_authority = 'trusted-tool-results'
),
CONSTRAINT ql3_result_rekey_identity_check CHECK (
overlay_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
artifact_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
from_key_id ~ '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$' AND
target_key_id ~ '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$' AND
from_key_id <> target_key_id AND
target_catalog_generation BETWEEN 1 AND 2147483647
),
CONSTRAINT ql3_result_rekey_digest_check CHECK (
(previous_overlay_digest IS NULL OR
previous_overlay_digest ~ '^[0-9a-f]{64}$') AND
source_binding_digest ~ '^[0-9a-f]{64}$' AND
target_catalog_digest ~ '^[0-9a-f]{64}$' AND
target_material_proof ~ '^[0-9a-f]{64}$' AND
command_digest ~ '^[0-9a-f]{64}$' AND
overlay_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_result_rekey_budget_check CHECK (
rekeyed_at_ms >= 0 AND
octet_length(overlay_json::text) BETWEEN 2 AND 393216
),
CONSTRAINT ql3_result_rekey_json_check CHECK (
jsonb_typeof(overlay_json) = 'object' AND
overlay_json @> jsonb_build_object(
'schema', 'qinglong/tool-execution-result-rekey-overlay@v1',
'overlayId', overlay_id,
'sourceBindingDigest', source_binding_digest,
'revision', revision,
'previousOverlayDigest', previous_overlay_digest,
'fromKeyId', from_key_id,
'rekeyedAtMs', rekeyed_at_ms,
'overlayDigest', overlay_digest
) AND
overlay_json -> 'sourceArtifact' ->> 'artifactId' = artifact_id AND
overlay_json -> 'targetCatalogFence' @> jsonb_build_object(
'generation', target_catalog_generation,
'catalogDigest', target_catalog_digest,
'keyId', target_key_id,
'materialProof', target_material_proof
)
)
)
`.trim(),
`CREATE UNIQUE INDEX ql3_result_rekey_mutation_key ON "ql3"."tool_execution_result_rekey_overlays" (mutation_id)`,
`CREATE UNIQUE INDEX ql3_result_rekey_artifact_revision_key ON "ql3"."tool_execution_result_rekey_overlays" (artifact_id, revision)`,
`CREATE UNIQUE INDEX ql3_result_rekey_artifact_revision_digest_key ON "ql3"."tool_execution_result_rekey_overlays" (artifact_id, revision, overlay_digest)`,
`CREATE INDEX ql3_result_rekey_artifact_idx ON "ql3"."tool_execution_result_rekey_overlays" (artifact_id, revision DESC)`,
`CREATE INDEX ql3_result_rekey_target_idx ON "ql3"."tool_execution_result_rekey_overlays" (target_key_id, artifact_id, revision DESC)`,
`
CREATE TABLE "ql3"."tool_execution_result_rekey_heads" (
artifact_id varchar(128) PRIMARY KEY,
revision integer NOT NULL,
overlay_id varchar(128) NOT NULL,
overlay_digest char(64) NOT NULL,
target_catalog_generation integer NOT NULL,
target_catalog_digest char(64) NOT NULL,
target_key_id varchar(128) NOT NULL,
updated_at_ms bigint NOT NULL,
CONSTRAINT ql3_result_rekey_head_overlay_fk FOREIGN KEY (
artifact_id, revision, overlay_digest
) REFERENCES "ql3"."tool_execution_result_rekey_overlays" (
artifact_id, revision, overlay_digest
) ON DELETE RESTRICT,
CONSTRAINT ql3_result_rekey_head_identity_check CHECK (
revision BETWEEN 1 AND 2147483647 AND
artifact_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
overlay_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
target_key_id ~ '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$' AND
target_catalog_generation BETWEEN 1 AND 2147483647 AND
updated_at_ms >= 0
),
CONSTRAINT ql3_result_rekey_head_digest_check CHECK (
overlay_digest ~ '^[0-9a-f]{64}$' AND
target_catalog_digest ~ '^[0-9a-f]{64}$'
)
)
`.trim(),
`CREATE UNIQUE INDEX ql3_result_rekey_head_overlay_key ON "ql3"."tool_execution_result_rekey_heads" (overlay_id)`,
`CREATE UNIQUE INDEX ql3_result_rekey_head_digest_key ON "ql3"."tool_execution_result_rekey_heads" (overlay_digest)`,
`CREATE INDEX ql3_result_rekey_head_target_idx ON "ql3"."tool_execution_result_rekey_heads" (target_key_id, artifact_id)`,
`
CREATE TABLE "ql3"."tool_result_key_retirement_receipts" (
receipt_digest char(64) PRIMARY KEY,
catalog_authority varchar(64) NOT NULL,
catalog_generation integer NOT NULL,
catalog_digest char(64) NOT NULL,
key_id varchar(128) NOT NULL,
material_proof char(64) NOT NULL,
mutation_id varchar(128) NOT NULL,
command_digest char(64) NOT NULL,
binding_count integer NOT NULL,
overlay_head_count integer NOT NULL,
uncovered_binding_count integer NOT NULL,
uncovered_overlay_head_count integer NOT NULL,
coverage_digest char(64) NOT NULL,
created_at_ms bigint NOT NULL,
receipt_json jsonb NOT NULL,
CONSTRAINT ql3_result_retirement_catalog_fk FOREIGN KEY (
catalog_authority, catalog_generation, catalog_digest
) REFERENCES "ql3"."tool_result_key_catalog_generations" (
authority, generation, catalog_digest
) ON DELETE RESTRICT,
CONSTRAINT ql3_result_retirement_authority_check CHECK (
catalog_authority = 'trusted-tool-results'
),
CONSTRAINT ql3_result_retirement_identity_check CHECK (
catalog_generation BETWEEN 1 AND 2147483647 AND
key_id ~ '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$' AND
mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
),
CONSTRAINT ql3_result_retirement_count_check CHECK (
binding_count BETWEEN 0 AND 2147483647 AND
overlay_head_count BETWEEN 0 AND 2147483647 AND
uncovered_binding_count = 0 AND
uncovered_overlay_head_count = 0 AND created_at_ms >= 0
),
CONSTRAINT ql3_result_retirement_digest_check CHECK (
receipt_digest ~ '^[0-9a-f]{64}$' AND
catalog_digest ~ '^[0-9a-f]{64}$' AND
material_proof ~ '^[0-9a-f]{64}$' AND
command_digest ~ '^[0-9a-f]{64}$' AND
coverage_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_result_retirement_json_check CHECK (
jsonb_typeof(receipt_json) = 'object' AND
receipt_json @> jsonb_build_object(
'schema', 'qinglong/tool-result-key-retirement-receipt@v1',
'catalogGeneration', catalog_generation,
'catalogDigest', catalog_digest,
'keyId', key_id,
'materialProof', material_proof,
'mutationId', mutation_id,
'bindingCount', binding_count,
'overlayHeadCount', overlay_head_count,
'uncoveredBindingCount', 0,
'uncoveredOverlayHeadCount', 0,
'coverageDigest', coverage_digest,
'createdAtMs', created_at_ms,
'receiptDigest', receipt_digest
)
)
)
`.trim(),
`CREATE UNIQUE INDEX ql3_result_retirement_mutation_key ON "ql3"."tool_result_key_retirement_receipts" (mutation_id)`,
`CREATE INDEX ql3_result_retirement_catalog_idx ON "ql3"."tool_result_key_retirement_receipts" (catalog_generation, key_id)`,
`
REVOKE ALL ON
"ql3"."tool_execution_result_rekey_overlays",
"ql3"."tool_execution_result_rekey_heads",
"ql3"."tool_result_key_retirement_receipts"
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_package_executor, ql3_worker_ingress
`.trim(),
`GRANT SELECT ON "ql3"."tool_execution_result_rekey_overlays", "ql3"."tool_execution_result_rekey_heads" TO ql3_runtime`,
`GRANT SELECT, INSERT ON "ql3"."tool_execution_result_rekey_overlays", "ql3"."tool_result_key_retirement_receipts" TO ql3_admin`,
`GRANT SELECT, INSERT, UPDATE ON "ql3"."tool_execution_result_rekey_heads" TO ql3_admin`,
`GRANT SELECT ON "ql3"."tool_execution_completions", "ql3"."tool_execution_result_key_bindings" TO ql3_admin`,
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 35,
migration_id = 'pg-0036-tool-result-rekey-overlays',
capabilities = '${CAPABILITIES_V35}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 34
AND migration_id = 'pg-0035-tool-result-key-catalog'
AND capabilities = '${CAPABILITIES_V34}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 34'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,779 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V35 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V36 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0037PluginPackageQuarantineMigration =
definePostgresSqlMigration({
id: 'pg-0037-plugin-package-quarantine',
statements: [
`
ALTER TABLE "ql3"."plugin_package_installs"
ADD CONSTRAINT ql3_plugin_package_installs_quarantine_target_key
UNIQUE (
project_id, package_name, installation_id, lock_digest, record_digest
)
`.trim(),
`
ALTER TABLE "ql3"."project_tool_definition_snapshots"
ADD CONSTRAINT ql3_project_tool_snapshot_withdrawal_key
UNIQUE (project_id, active_vector_digest, snapshot_digest)
`.trim(),
`
CREATE TABLE "ql3"."plugin_package_quarantine_events" (
event_digest char(64) PRIMARY KEY,
mutation_id varchar(128) NOT NULL,
revocation_receipt_digest char(64) NOT NULL,
impact_digest char(64) NOT NULL,
project_id varchar(128) NOT NULL,
package_name varchar(63) NOT NULL,
installation_id varchar(128) NOT NULL,
lock_digest char(64) NOT NULL,
install_state varchar(16) NOT NULL,
install_version integer NOT NULL,
install_record_digest char(64) NOT NULL,
active_lock_digest char(64),
proposer_type varchar(16) NOT NULL,
proposer_id varchar(255) NOT NULL,
confirmer_type varchar(16) NOT NULL,
confirmer_id varchar(255) NOT NULL,
authorization_mode varchar(16) NOT NULL,
reason_code varchar(32) NOT NULL,
occurred_at_ms bigint NOT NULL,
event_json jsonb NOT NULL,
CONSTRAINT ql3_plugin_package_quarantine_install_fk FOREIGN KEY (
project_id, package_name, installation_id, lock_digest,
install_record_digest
) REFERENCES "ql3"."plugin_package_installs" (
project_id, package_name, installation_id, lock_digest, record_digest
) ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_quarantine_identity_check CHECK (
mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$' AND
installation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
install_state IN ('queued', 'staged', 'activating', 'active') AND
install_version BETWEEN 1 AND 2147483647
),
CONSTRAINT ql3_plugin_package_quarantine_state_check CHECK (
(install_state = 'active' AND active_lock_digest = lock_digest) OR
(install_state <> 'active' AND
(active_lock_digest IS NULL OR active_lock_digest <> lock_digest))
),
CONSTRAINT ql3_plugin_package_quarantine_subject_check CHECK (
proposer_type IN (
'user', 'api_app', 'mcp_client', 'agent', 'system', 'worker'
) AND
confirmer_type IN (
'user', 'api_app', 'mcp_client', 'agent', 'system', 'worker'
) AND
octet_length(proposer_id) BETWEEN 1 AND 255 AND
octet_length(confirmer_id) BETWEEN 1 AND 255 AND
authorization_mode IN ('dual_control', 'break_glass') AND
(authorization_mode = 'break_glass' OR
proposer_type <> confirmer_type OR proposer_id <> confirmer_id) AND
reason_code IN (
'suspected_key_compromise', 'confirmed_key_compromise'
)
),
CONSTRAINT ql3_plugin_package_quarantine_digest_check CHECK (
event_digest ~ '^[0-9a-f]{64}$' AND
revocation_receipt_digest ~ '^[0-9a-f]{64}$' AND
impact_digest ~ '^[0-9a-f]{64}$' AND
lock_digest ~ '^[0-9a-f]{64}$' AND
install_record_digest ~ '^[0-9a-f]{64}$' AND
(active_lock_digest IS NULL OR
active_lock_digest ~ '^[0-9a-f]{64}$')
),
CONSTRAINT ql3_plugin_package_quarantine_json_check CHECK (
jsonb_typeof(event_json) = 'object' AND
octet_length(event_json::text) BETWEEN 2 AND 262144 AND
event_json @> jsonb_build_object(
'schema', 'qinglong/plugin-package-quarantine-event@v1',
'mutationId', mutation_id,
'revocationReceiptDigest', revocation_receipt_digest,
'impactDigest', impact_digest,
'authorizationMode', authorization_mode,
'reasonCode', reason_code,
'occurredAtMs', occurred_at_ms,
'eventDigest', event_digest
) AND
event_json -> 'target' @> jsonb_build_object(
'projectId', project_id,
'packageName', package_name,
'installationId', installation_id,
'lockDigest', lock_digest,
'installState', install_state,
'installVersion', install_version,
'installRecordDigest', install_record_digest,
'activeLockDigest', active_lock_digest
) AND
event_json -> 'proposer' @> jsonb_build_object(
'type', proposer_type, 'id', proposer_id
) AND
event_json -> 'confirmer' @> jsonb_build_object(
'type', confirmer_type, 'id', confirmer_id
)
),
CONSTRAINT ql3_plugin_package_quarantine_time_check CHECK (
occurred_at_ms >= 0
)
)
`.trim(),
`CREATE UNIQUE INDEX ql3_plugin_package_quarantine_mutation_key ON "ql3"."plugin_package_quarantine_events" (mutation_id)`,
`CREATE UNIQUE INDEX ql3_plugin_package_quarantine_target_key ON "ql3"."plugin_package_quarantine_events" (project_id, package_name, installation_id, lock_digest)`,
`CREATE INDEX ql3_plugin_package_quarantine_lock_idx ON "ql3"."plugin_package_quarantine_events" (lock_digest, project_id, package_name)`,
`CREATE INDEX ql3_plugin_package_quarantine_project_idx ON "ql3"."plugin_package_quarantine_events" (project_id, package_name, occurred_at_ms, event_digest)`,
`
CREATE TABLE "ql3"."plugin_package_withdrawal_receipts" (
event_digest char(64) PRIMARY KEY,
receipt_digest char(64) NOT NULL,
project_id varchar(128) NOT NULL,
capability_status varchar(16) NOT NULL,
task_count integer NOT NULL,
previous_active_vector_digest char(64),
current_active_vector_digest char(64),
current_tool_snapshot_digest char(64),
retained_source_count integer NOT NULL,
committed_at_ms bigint NOT NULL,
receipt_json jsonb NOT NULL,
CONSTRAINT ql3_plugin_package_withdrawal_event_fk FOREIGN KEY (
event_digest
) REFERENCES "ql3"."plugin_package_quarantine_events" (event_digest)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_withdrawal_snapshot_fk FOREIGN KEY (
project_id, current_active_vector_digest, current_tool_snapshot_digest
) REFERENCES "ql3"."project_tool_definition_snapshots" (
project_id, active_vector_digest, snapshot_digest
) ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_withdrawal_disposition_check CHECK (
(
capability_status = 'not_active' AND task_count = 0 AND
previous_active_vector_digest IS NULL AND
current_active_vector_digest IS NULL AND
current_tool_snapshot_digest IS NULL AND retained_source_count = 0
) OR (
capability_status = 'withdrawn' AND task_count BETWEEN 0 AND 128 AND
previous_active_vector_digest IS NOT NULL AND
current_active_vector_digest IS NOT NULL AND
previous_active_vector_digest <> current_active_vector_digest AND
current_tool_snapshot_digest IS NOT NULL AND
retained_source_count BETWEEN 0 AND 128
)
),
CONSTRAINT ql3_plugin_package_withdrawal_digest_check CHECK (
event_digest ~ '^[0-9a-f]{64}$' AND
receipt_digest ~ '^[0-9a-f]{64}$' AND
(previous_active_vector_digest IS NULL OR
previous_active_vector_digest ~ '^[0-9a-f]{64}$') AND
(current_active_vector_digest IS NULL OR
current_active_vector_digest ~ '^[0-9a-f]{64}$') AND
(current_tool_snapshot_digest IS NULL OR
current_tool_snapshot_digest ~ '^[0-9a-f]{64}$')
),
CONSTRAINT ql3_plugin_package_withdrawal_json_check CHECK (
jsonb_typeof(receipt_json) = 'object' AND
octet_length(receipt_json::text) BETWEEN 2 AND 8388608 AND
receipt_json @> jsonb_build_object(
'schema', 'qinglong/plugin-package-withdrawal-receipt@v1',
'eventDigest', event_digest,
'committedAtMs', committed_at_ms,
'receiptDigest', receipt_digest
) AND
receipt_json -> 'target' ->> 'projectId' = project_id AND
receipt_json -> 'capability' @> jsonb_build_object(
'status', capability_status,
'previousActiveVectorDigest', previous_active_vector_digest,
'currentActiveVectorDigest', current_active_vector_digest,
'currentToolSnapshotDigest', current_tool_snapshot_digest,
'retainedSourceCount', retained_source_count
) AND
jsonb_array_length(
receipt_json -> 'capability' -> 'taskWithdrawals'
) = task_count
),
CONSTRAINT ql3_plugin_package_withdrawal_time_check CHECK (
committed_at_ms >= 0
)
)
`.trim(),
`CREATE UNIQUE INDEX ql3_plugin_package_withdrawal_receipt_key ON "ql3"."plugin_package_withdrawal_receipts" (receipt_digest)`,
`CREATE INDEX ql3_plugin_package_withdrawal_snapshot_idx ON "ql3"."plugin_package_withdrawal_receipts" (current_tool_snapshot_digest, event_digest)`,
`
CREATE TABLE "ql3"."plugin_package_withdrawal_tasks" (
event_digest char(64) NOT NULL,
project_id varchar(128) NOT NULL,
task_id varchar(128) NOT NULL,
previous_revision integer NOT NULL,
disabled_revision integer NOT NULL,
previous_content_digest char(64) NOT NULL,
disabled_content_digest char(64) NOT NULL,
PRIMARY KEY (event_digest, task_id),
CONSTRAINT ql3_plugin_package_withdrawal_task_receipt_fk FOREIGN KEY (
event_digest
) REFERENCES "ql3"."plugin_package_withdrawal_receipts" (event_digest)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_withdrawal_task_previous_fk FOREIGN KEY (
project_id, task_id, previous_revision
) REFERENCES "ql3"."task_definition_revisions" (
project_id, task_id, revision
) ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_withdrawal_task_disabled_fk FOREIGN KEY (
project_id, task_id, disabled_revision
) REFERENCES "ql3"."task_definition_revisions" (
project_id, task_id, revision
) ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_withdrawal_task_identity_check CHECK (
task_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
previous_revision BETWEEN 1 AND 2147483646 AND
disabled_revision = previous_revision + 1
),
CONSTRAINT ql3_plugin_package_withdrawal_task_digest_check CHECK (
event_digest ~ '^[0-9a-f]{64}$' AND
previous_content_digest ~ '^[0-9a-f]{64}$' AND
disabled_content_digest ~ '^[0-9a-f]{64}$'
)
)
`.trim(),
`CREATE INDEX ql3_plugin_package_withdrawal_task_idx ON "ql3"."plugin_package_withdrawal_tasks" (project_id, task_id, event_digest)`,
`
CREATE FUNCTION "ql3"."plugin_package_run_start_allowed"(
p_project_id varchar,
p_task_id varchar,
p_task_revision varchar
)
RETURNS boolean
LANGUAGE plpgsql
VOLATILE
SECURITY DEFINER
SET search_path = pg_catalog, ql3
AS $ql3$
DECLARE
project_exists boolean;
BEGIN
IF NOT pg_has_role(session_user, 'ql3_runtime', 'member') THEN
RAISE EXCEPTION 'Runtime authority is required'
USING ERRCODE = 'insufficient_privilege';
END IF;
SELECT true INTO project_exists
FROM "ql3"."projects"
WHERE id = p_project_id
FOR SHARE;
IF NOT COALESCE(project_exists, false) THEN
RETURN false;
END IF;
RETURN NOT EXISTS (
SELECT 1
FROM "ql3"."plugin_package_quarantine_events" AS quarantine
JOIN "ql3"."plugin_package_task_reconciliations" AS reconciliation
ON reconciliation.project_id = quarantine.project_id
AND reconciliation.package_name = quarantine.package_name
AND reconciliation.lock_digest = quarantine.lock_digest
JOIN "ql3"."plugin_package_task_reconciliation_items" AS item
ON item.generation_digest = reconciliation.generation_digest
AND item.task_id = p_task_id
AND 'qltd:v1:' || item.revision || ':' || item.content_digest =
p_task_revision
WHERE quarantine.project_id = p_project_id
);
END
$ql3$
`.trim(),
`
CREATE FUNCTION "ql3"."plugin_package_tool_start_allowed"(
p_project_id varchar,
p_definition_ref varchar,
p_definition_digest char(64)
)
RETURNS boolean
LANGUAGE plpgsql
VOLATILE
SECURITY DEFINER
SET search_path = pg_catalog, ql3
AS $ql3$
DECLARE
project_exists boolean;
BEGIN
IF NOT pg_has_role(session_user, 'ql3_runtime', 'member') THEN
RAISE EXCEPTION 'Runtime authority is required'
USING ERRCODE = 'insufficient_privilege';
END IF;
SELECT true INTO project_exists
FROM "ql3"."projects"
WHERE id = p_project_id
FOR SHARE;
IF NOT COALESCE(project_exists, false) THEN
RETURN false;
END IF;
RETURN NOT EXISTS (
SELECT 1
FROM "ql3"."plugin_package_quarantine_events" AS quarantine
JOIN "ql3"."project_tool_definition_snapshot_sources" AS source
ON source.project_id = quarantine.project_id
AND source.package_name = quarantine.package_name
AND source.installation_id = quarantine.installation_id
AND source.lock_digest = quarantine.lock_digest
JOIN "ql3"."project_tool_definition_snapshots" AS snapshot
ON snapshot.project_id = source.project_id
AND snapshot.active_vector_digest = source.active_vector_digest
CROSS JOIN LATERAL jsonb_array_elements(
snapshot.snapshot_json -> 'definitions'
) AS definition(item)
WHERE quarantine.project_id = p_project_id
AND definition.item ->> 'packageName' = quarantine.package_name
AND 'tool:' ||
(definition.item -> 'definition' ->> 'name') || '@' ||
(definition.item -> 'definition' ->> 'version') = p_definition_ref
AND definition.item ->> 'definitionDigest' = p_definition_digest
);
END
$ql3$
`.trim(),
`
CREATE FUNCTION "ql3"."commit_plugin_package_quarantine"(
p_event jsonb,
p_receipt jsonb,
p_task_writes jsonb,
p_snapshot jsonb
)
RETURNS boolean
LANGUAGE plpgsql
VOLATILE
SECURITY DEFINER
SET search_path = pg_catalog, ql3
AS $ql3$
DECLARE
event_digest_value char(64);
project_id_value varchar(128);
package_name_value varchar(63);
installation_id_value varchar(128);
lock_digest_value char(64);
install_record "ql3"."plugin_package_installs"%ROWTYPE;
existing_event jsonb;
existing_receipt jsonb;
task_write jsonb;
previous_task "ql3"."task_definition_revisions"%ROWTYPE;
disabled_task jsonb;
withdrawal jsonb;
enabled_task_count integer;
source_count integer;
inserted_snapshot integer;
BEGIN
IF NOT pg_has_role(session_user, 'ql3_package_executor', 'member') THEN
RAISE EXCEPTION 'Package executor authority is required'
USING ERRCODE = 'insufficient_privilege';
END IF;
IF jsonb_typeof(p_event) <> 'object'
OR jsonb_typeof(p_receipt) <> 'object'
OR jsonb_typeof(p_task_writes) <> 'array'
OR jsonb_array_length(p_task_writes) > 128
OR (p_snapshot <> 'null'::jsonb AND
jsonb_typeof(p_snapshot) <> 'object') THEN
RAISE EXCEPTION 'invalid Package quarantine input'
USING ERRCODE = 'check_violation';
END IF;
event_digest_value := p_event ->> 'eventDigest';
project_id_value := p_event -> 'target' ->> 'projectId';
package_name_value := p_event -> 'target' ->> 'packageName';
installation_id_value := p_event -> 'target' ->> 'installationId';
lock_digest_value := p_event -> 'target' ->> 'lockDigest';
IF p_receipt ->> 'eventDigest' IS DISTINCT FROM event_digest_value
OR p_receipt -> 'target' IS DISTINCT FROM p_event -> 'target'
OR (p_receipt ->> 'committedAtMs')::bigint <
(p_event ->> 'occurredAtMs')::bigint THEN
RAISE EXCEPTION 'Package quarantine receipt does not bind the event'
USING ERRCODE = 'check_violation';
END IF;
PERFORM 1 FROM "ql3"."projects"
WHERE id = project_id_value
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'Package quarantine Project is absent'
USING ERRCODE = 'check_violation';
END IF;
SELECT event_json INTO existing_event
FROM "ql3"."plugin_package_quarantine_events"
WHERE event_digest = event_digest_value;
IF FOUND THEN
SELECT receipt_json INTO existing_receipt
FROM "ql3"."plugin_package_withdrawal_receipts"
WHERE event_digest = event_digest_value;
IF NOT FOUND
OR existing_event IS DISTINCT FROM p_event
OR existing_receipt IS DISTINCT FROM p_receipt THEN
RAISE EXCEPTION 'Package quarantine replay conflicts'
USING ERRCODE = 'unique_violation';
END IF;
RETURN false;
END IF;
SELECT * INTO install_record
FROM "ql3"."plugin_package_installs"
WHERE installation_id = installation_id_value
FOR UPDATE;
IF NOT FOUND
OR install_record.project_id <> project_id_value
OR install_record.package_name <> package_name_value
OR install_record.lock_digest <> lock_digest_value
OR install_record.state <> p_event -> 'target' ->> 'installState'
OR install_record.version <>
(p_event -> 'target' ->> 'installVersion')::integer
OR install_record.record_digest <>
p_event -> 'target' ->> 'installRecordDigest'
OR install_record.active_lock_digest IS DISTINCT FROM
p_event -> 'target' ->> 'activeLockDigest' THEN
RAISE EXCEPTION 'Package quarantine target drifted'
USING ERRCODE = 'check_violation';
END IF;
IF install_record.state = 'active' AND NOT EXISTS (
SELECT 1
FROM "ql3"."plugin_package_install_heads" AS head
JOIN "ql3"."plugin_package_installs" AS head_install
ON head_install.installation_id = head.installation_id
JOIN "ql3"."plugin_package_materialized_revisions" AS revision
ON revision.project_id = install_record.project_id
AND revision.package_name = install_record.package_name
AND revision.generation = install_record.target_generation
AND revision.lock_digest = install_record.lock_digest
WHERE head.project_id = install_record.project_id
AND head.package_name = install_record.package_name
AND head.installation_id = install_record.installation_id
AND head_install.active_lock_digest = install_record.lock_digest
) THEN
RAISE EXCEPTION 'active Package quarantine target is not materialized'
USING ERRCODE = 'check_violation';
END IF;
INSERT INTO "ql3"."plugin_package_quarantine_events" (
event_digest, mutation_id, revocation_receipt_digest, impact_digest,
project_id, package_name, installation_id, lock_digest,
install_state, install_version, install_record_digest,
active_lock_digest, proposer_type, proposer_id, confirmer_type,
confirmer_id, authorization_mode, reason_code, occurred_at_ms, event_json
) VALUES (
event_digest_value,
p_event ->> 'mutationId',
p_event ->> 'revocationReceiptDigest',
p_event ->> 'impactDigest',
project_id_value,
package_name_value,
installation_id_value,
lock_digest_value,
p_event -> 'target' ->> 'installState',
(p_event -> 'target' ->> 'installVersion')::integer,
p_event -> 'target' ->> 'installRecordDigest',
p_event -> 'target' ->> 'activeLockDigest',
p_event -> 'proposer' ->> 'type',
p_event -> 'proposer' ->> 'id',
p_event -> 'confirmer' ->> 'type',
p_event -> 'confirmer' ->> 'id',
p_event ->> 'authorizationMode',
p_event ->> 'reasonCode',
(p_event ->> 'occurredAtMs')::bigint,
p_event
);
IF install_record.state <> 'active' THEN
IF p_snapshot <> 'null'::jsonb OR jsonb_array_length(p_task_writes) <> 0
OR p_receipt -> 'capability' ->> 'status' <> 'not_active' THEN
RAISE EXCEPTION 'inactive Package quarantine disposition is invalid'
USING ERRCODE = 'check_violation';
END IF;
ELSE
IF p_snapshot = 'null'::jsonb
OR p_receipt -> 'capability' ->> 'status' <> 'withdrawn' THEN
RAISE EXCEPTION 'active Package quarantine disposition is invalid'
USING ERRCODE = 'check_violation';
END IF;
SELECT count(*)::integer INTO source_count
FROM "ql3"."plugin_package_install_heads" AS head
JOIN "ql3"."plugin_package_installs" AS head_install
ON head_install.installation_id = head.installation_id
JOIN "ql3"."plugin_package_installs" AS active_install
ON active_install.project_id = head.project_id
AND active_install.package_name = head.package_name
AND active_install.lock_digest = head_install.active_lock_digest
JOIN "ql3"."plugin_package_materialized_revisions" AS revision
ON revision.project_id = active_install.project_id
AND revision.package_name = active_install.package_name
AND revision.generation = active_install.target_generation
AND revision.lock_digest = active_install.lock_digest
LEFT JOIN "ql3"."plugin_package_quarantine_events" AS quarantine
ON quarantine.project_id = active_install.project_id
AND quarantine.package_name = active_install.package_name
AND quarantine.installation_id = active_install.installation_id
AND quarantine.lock_digest = active_install.lock_digest
WHERE head.project_id = project_id_value
AND head_install.active_lock_digest IS NOT NULL
AND active_install.state = 'active'
AND quarantine.event_digest IS NULL;
IF source_count > 128
OR source_count <>
jsonb_array_length(p_snapshot -> 'sources')
OR source_count <>
(p_receipt -> 'capability' ->> 'retainedSourceCount')::integer
OR EXISTS (
SELECT 1
FROM "ql3"."plugin_package_install_heads" AS head
JOIN "ql3"."plugin_package_installs" AS head_install
ON head_install.installation_id = head.installation_id
JOIN "ql3"."plugin_package_installs" AS active_install
ON active_install.project_id = head.project_id
AND active_install.package_name = head.package_name
AND active_install.lock_digest = head_install.active_lock_digest
JOIN "ql3"."plugin_package_materialized_revisions" AS revision
ON revision.project_id = active_install.project_id
AND revision.package_name = active_install.package_name
AND revision.generation = active_install.target_generation
AND revision.lock_digest = active_install.lock_digest
LEFT JOIN "ql3"."plugin_package_quarantine_events" AS quarantine
ON quarantine.project_id = active_install.project_id
AND quarantine.package_name = active_install.package_name
AND quarantine.installation_id = active_install.installation_id
AND quarantine.lock_digest = active_install.lock_digest
WHERE head.project_id = project_id_value
AND head_install.active_lock_digest IS NOT NULL
AND active_install.state = 'active'
AND quarantine.event_digest IS NULL
AND NOT EXISTS (
SELECT 1 FROM jsonb_array_elements(
p_snapshot -> 'sources'
) AS source
WHERE source ->> 'packageName' = active_install.package_name
AND source ->> 'installationId' =
active_install.installation_id
AND (source ->> 'generation')::integer =
active_install.target_generation
AND source ->> 'generationDigest' =
revision.generation_digest
AND source ->> 'lockDigest' = active_install.lock_digest
AND source ->> 'revisionDigest' =
revision.revision_digest
)
) THEN
RAISE EXCEPTION 'Package quarantine Tool source vector drifted'
USING ERRCODE = 'check_violation';
END IF;
SELECT count(*)::integer INTO enabled_task_count
FROM "ql3"."plugin_package_task_ownerships" AS ownership
JOIN "ql3"."task_definitions" AS head
ON head.project_id = ownership.project_id
AND head.task_id = ownership.task_id
JOIN "ql3"."task_definition_revisions" AS revision
ON revision.project_id = head.project_id
AND revision.task_id = head.task_id
AND revision.revision = head.current_revision
WHERE ownership.project_id = project_id_value
AND ownership.package_name = package_name_value
AND revision.enabled = true;
IF enabled_task_count > 128
OR enabled_task_count <> jsonb_array_length(p_task_writes)
OR enabled_task_count <>
jsonb_array_length(
p_receipt -> 'capability' -> 'taskWithdrawals'
) THEN
RAISE EXCEPTION 'Package quarantine Task set drifted'
USING ERRCODE = 'check_violation';
END IF;
FOR task_write IN SELECT value FROM jsonb_array_elements(p_task_writes)
LOOP
disabled_task := task_write -> 'disabled';
withdrawal := task_write -> 'withdrawal';
SELECT revision.* INTO previous_task
FROM "ql3"."plugin_package_task_ownerships" AS ownership
JOIN "ql3"."task_definitions" AS head
ON head.project_id = ownership.project_id
AND head.task_id = ownership.task_id
JOIN "ql3"."task_definition_revisions" AS revision
ON revision.project_id = head.project_id
AND revision.task_id = head.task_id
AND revision.revision = head.current_revision
WHERE ownership.project_id = project_id_value
AND ownership.package_name = package_name_value
AND ownership.task_id = withdrawal ->> 'taskId'
AND revision.enabled = true
FOR UPDATE OF head;
IF NOT FOUND
OR previous_task.revision <>
(withdrawal ->> 'previousRevision')::integer
OR previous_task.content_digest <>
withdrawal ->> 'previousContentDigest'
OR disabled_task ->> 'projectId' <> project_id_value
OR disabled_task ->> 'taskId' <> withdrawal ->> 'taskId'
OR (disabled_task ->> 'revision')::integer <>
previous_task.revision + 1
OR (disabled_task ->> 'enabled')::boolean <> false
OR disabled_task ->> 'contentDigest' <>
withdrawal ->> 'disabledContentDigest' THEN
RAISE EXCEPTION 'Package quarantine Task write drifted'
USING ERRCODE = 'check_violation';
END IF;
INSERT INTO "ql3"."task_definition_revisions" (
project_id, task_id, revision, mutation_id, name, description,
kind, spec_json, labels_json, enabled, content_digest, created_at_ms
) VALUES (
project_id_value,
disabled_task ->> 'taskId',
(disabled_task ->> 'revision')::integer,
(disabled_task ->> 'mutationId')::uuid,
disabled_task ->> 'name',
disabled_task ->> 'description',
disabled_task ->> 'kind',
disabled_task -> 'spec',
disabled_task -> 'labels',
false,
disabled_task ->> 'contentDigest',
(disabled_task ->> 'updatedAtMs')::bigint
);
UPDATE "ql3"."task_definitions"
SET current_revision = (disabled_task ->> 'revision')::integer,
updated_at_ms = (disabled_task ->> 'updatedAtMs')::bigint
WHERE project_id = project_id_value
AND task_id = disabled_task ->> 'taskId'
AND current_revision = previous_task.revision;
IF NOT FOUND THEN
RAISE EXCEPTION 'Package quarantine Task head drifted'
USING ERRCODE = 'serialization_failure';
END IF;
END LOOP;
INSERT INTO "ql3"."project_tool_definition_snapshots" (
project_id, active_vector_digest, definitions_digest,
snapshot_digest, snapshot_json, committed_at_ms
) VALUES (
project_id_value,
p_snapshot ->> 'activeVectorDigest',
p_snapshot ->> 'definitionsDigest',
p_snapshot ->> 'snapshotDigest',
p_snapshot,
(p_receipt ->> 'committedAtMs')::bigint
)
ON CONFLICT (project_id, active_vector_digest) DO NOTHING;
GET DIAGNOSTICS inserted_snapshot = ROW_COUNT;
IF inserted_snapshot = 1 THEN
INSERT INTO "ql3"."project_tool_definition_snapshot_sources" (
project_id, active_vector_digest, package_name, installation_id,
generation, generation_digest, lock_digest, revision_digest
)
SELECT
project_id_value,
p_snapshot ->> 'activeVectorDigest',
source ->> 'packageName',
source ->> 'installationId',
(source ->> 'generation')::integer,
source ->> 'generationDigest',
source ->> 'lockDigest',
source ->> 'revisionDigest'
FROM jsonb_array_elements(p_snapshot -> 'sources') AS source;
END IF;
IF NOT EXISTS (
SELECT 1
FROM "ql3"."project_tool_definition_snapshots"
WHERE project_id = project_id_value
AND active_vector_digest = p_snapshot ->> 'activeVectorDigest'
AND snapshot_json = p_snapshot
) THEN
RAISE EXCEPTION 'Package quarantine snapshot conflicts'
USING ERRCODE = 'unique_violation';
END IF;
END IF;
INSERT INTO "ql3"."plugin_package_withdrawal_receipts" (
event_digest, receipt_digest, project_id, capability_status,
task_count, previous_active_vector_digest,
current_active_vector_digest, current_tool_snapshot_digest,
retained_source_count, committed_at_ms, receipt_json
) VALUES (
event_digest_value,
p_receipt ->> 'receiptDigest',
project_id_value,
p_receipt -> 'capability' ->> 'status',
jsonb_array_length(
p_receipt -> 'capability' -> 'taskWithdrawals'
),
p_receipt -> 'capability' ->> 'previousActiveVectorDigest',
p_receipt -> 'capability' ->> 'currentActiveVectorDigest',
p_receipt -> 'capability' ->> 'currentToolSnapshotDigest',
(p_receipt -> 'capability' ->> 'retainedSourceCount')::integer,
(p_receipt ->> 'committedAtMs')::bigint,
p_receipt
);
INSERT INTO "ql3"."plugin_package_withdrawal_tasks" (
event_digest, project_id, task_id, previous_revision,
disabled_revision, previous_content_digest, disabled_content_digest
)
SELECT
event_digest_value,
project_id_value,
receipt_withdrawal ->> 'taskId',
(receipt_withdrawal ->> 'previousRevision')::integer,
(receipt_withdrawal ->> 'disabledRevision')::integer,
receipt_withdrawal ->> 'previousContentDigest',
receipt_withdrawal ->> 'disabledContentDigest'
FROM jsonb_array_elements(
p_receipt -> 'capability' -> 'taskWithdrawals'
) AS receipt_withdrawal;
RETURN true;
END
$ql3$
`.trim(),
`
REVOKE ALL ON
"ql3"."plugin_package_quarantine_events",
"ql3"."plugin_package_withdrawal_receipts",
"ql3"."plugin_package_withdrawal_tasks"
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_package_executor, ql3_worker_ingress
`.trim(),
`
REVOKE ALL ON FUNCTION
"ql3"."plugin_package_run_start_allowed"(varchar, varchar, varchar),
"ql3"."plugin_package_tool_start_allowed"(varchar, varchar, char(64)),
"ql3"."commit_plugin_package_quarantine"(jsonb, jsonb, jsonb, jsonb)
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_package_executor, ql3_worker_ingress
`.trim(),
`GRANT SELECT ON "ql3"."plugin_package_quarantine_events", "ql3"."plugin_package_withdrawal_receipts", "ql3"."plugin_package_withdrawal_tasks" TO ql3_package_executor`,
`GRANT EXECUTE ON FUNCTION "ql3"."plugin_package_run_start_allowed"(varchar, varchar, varchar), "ql3"."plugin_package_tool_start_allowed"(varchar, varchar, char(64)) TO ql3_runtime`,
`GRANT EXECUTE ON FUNCTION "ql3"."commit_plugin_package_quarantine"(jsonb, jsonb, jsonb, jsonb) TO ql3_package_executor`,
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 36,
migration_id = 'pg-0037-plugin-package-quarantine',
capabilities = '${CAPABILITIES_V36}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 35
AND migration_id = 'pg-0036-tool-result-rekey-overlays'
AND capabilities = '${CAPABILITIES_V35}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 35'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,426 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V36 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V37 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0038PluginPackagePublisherProvenanceMigration =
definePostgresSqlMigration({
id: 'pg-0038-plugin-package-publisher-provenance',
statements: [
`
CREATE TABLE "ql3"."plugin_package_publisher_provenance" (
installation_id varchar(128) PRIMARY KEY,
project_id varchar(128) NOT NULL,
package_name varchar(63) NOT NULL,
lock_digest char(64) NOT NULL,
artifact_digest char(64) NOT NULL,
manifest_digest char(64) NOT NULL,
content_digest char(64) NOT NULL,
stage_evidence_digest char(64) NOT NULL,
publisher varchar(253) NOT NULL,
key_id varchar(128) NOT NULL,
signature_digest char(64) NOT NULL,
key_not_before_ms bigint NOT NULL,
key_not_after_ms bigint NOT NULL,
verified_at_ms bigint NOT NULL,
provenance_digest char(64) NOT NULL,
provenance_json jsonb NOT NULL,
CONSTRAINT ql3_plugin_package_publisher_provenance_install_fk
FOREIGN KEY (installation_id)
REFERENCES "ql3"."plugin_package_installs" (installation_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_publisher_provenance_identity_check CHECK (
installation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$' AND
publisher ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$' AND
key_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
),
CONSTRAINT ql3_plugin_package_publisher_provenance_digest_check CHECK (
lock_digest ~ '^[0-9a-f]{64}$' AND
artifact_digest ~ '^[0-9a-f]{64}$' AND
manifest_digest ~ '^[0-9a-f]{64}$' AND
content_digest ~ '^[0-9a-f]{64}$' AND
stage_evidence_digest ~ '^[0-9a-f]{64}$' AND
signature_digest ~ '^[0-9a-f]{64}$' AND
provenance_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_plugin_package_publisher_provenance_time_check CHECK (
key_not_before_ms >= 0 AND
key_not_after_ms > key_not_before_ms AND
verified_at_ms >= key_not_before_ms AND
verified_at_ms < key_not_after_ms
),
CONSTRAINT ql3_plugin_package_publisher_provenance_json_check CHECK (
jsonb_typeof(provenance_json) = 'object' AND
octet_length(provenance_json::text) BETWEEN 2 AND 262144 AND
provenance_json @> jsonb_build_object(
'schema', 'qinglong/plugin-package-publisher-provenance@v1',
'projectId', project_id,
'packageName', package_name,
'installationId', installation_id,
'lockDigest', lock_digest,
'artifactDigest', artifact_digest,
'manifestDigest', manifest_digest,
'contentDigest', content_digest,
'stageEvidenceDigest', stage_evidence_digest,
'publisher', publisher,
'keyId', key_id,
'signatureDigest', signature_digest,
'keyNotBeforeMs', key_not_before_ms,
'keyNotAfterMs', key_not_after_ms,
'verifiedAtMs', verified_at_ms,
'provenanceDigest', provenance_digest
)
)
)
`.trim(),
`CREATE UNIQUE INDEX ql3_plugin_package_publisher_provenance_digest_key ON "ql3"."plugin_package_publisher_provenance" (provenance_digest)`,
`CREATE INDEX ql3_plugin_package_publisher_provenance_signer_idx ON "ql3"."plugin_package_publisher_provenance" (publisher, key_id, project_id, package_name, installation_id)`,
`CREATE INDEX ql3_plugin_package_publisher_provenance_lock_idx ON "ql3"."plugin_package_publisher_provenance" (lock_digest, installation_id)`,
`
CREATE TABLE "ql3"."plugin_package_publisher_revocation_receipts" (
receipt_digest char(64) PRIMARY KEY,
mutation_id varchar(128) NOT NULL,
publisher varchar(253) NOT NULL,
key_id varchar(128) NOT NULL,
previous_trust_digest char(64) NOT NULL,
current_trust_digest char(64) NOT NULL,
proposer_type varchar(16) NOT NULL,
proposer_id varchar(255) NOT NULL,
confirmer_type varchar(16) NOT NULL,
confirmer_id varchar(255) NOT NULL,
authorization_mode varchar(16) NOT NULL,
reason_code varchar(32) NOT NULL,
revoked_at_ms bigint NOT NULL,
receipt_json jsonb NOT NULL,
CONSTRAINT ql3_plugin_package_publisher_revocation_identity_check CHECK (
mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
publisher ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$' AND
key_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
proposer_type IN (
'user', 'api_app', 'mcp_client', 'agent', 'system', 'worker'
) AND
confirmer_type IN (
'user', 'api_app', 'mcp_client', 'agent', 'system', 'worker'
) AND
octet_length(proposer_id) BETWEEN 1 AND 255 AND
octet_length(confirmer_id) BETWEEN 1 AND 255 AND
authorization_mode IN ('dual_control', 'break_glass') AND
(authorization_mode = 'break_glass' OR
proposer_type <> confirmer_type OR proposer_id <> confirmer_id) AND
reason_code IN (
'suspected_key_compromise', 'confirmed_key_compromise'
)
),
CONSTRAINT ql3_plugin_package_publisher_revocation_digest_check CHECK (
receipt_digest ~ '^[0-9a-f]{64}$' AND
previous_trust_digest ~ '^[0-9a-f]{64}$' AND
current_trust_digest ~ '^[0-9a-f]{64}$' AND
previous_trust_digest <> current_trust_digest
),
CONSTRAINT ql3_plugin_package_publisher_revocation_time_check CHECK (
revoked_at_ms >= 0
),
CONSTRAINT ql3_plugin_package_publisher_revocation_json_check CHECK (
jsonb_typeof(receipt_json) = 'object' AND
octet_length(receipt_json::text) BETWEEN 2 AND 262144 AND
receipt_json @> jsonb_build_object(
'schema',
'qinglong/plugin-package-publisher-key-revocation-receipt@v1',
'mutationId', mutation_id,
'publisher', publisher,
'keyId', key_id,
'previousTrustDigest', previous_trust_digest,
'currentTrustDigest', current_trust_digest,
'authorizationMode', authorization_mode,
'reasonCode', reason_code,
'revokedAtMs', revoked_at_ms,
'receiptDigest', receipt_digest
) AND
receipt_json -> 'proposer' @> jsonb_build_object(
'type', proposer_type, 'id', proposer_id
) AND
receipt_json -> 'confirmer' @> jsonb_build_object(
'type', confirmer_type, 'id', confirmer_id
)
)
)
`.trim(),
`CREATE UNIQUE INDEX ql3_plugin_package_publisher_revocation_mutation_key ON "ql3"."plugin_package_publisher_revocation_receipts" (mutation_id)`,
`CREATE UNIQUE INDEX ql3_plugin_package_publisher_revocation_signer_key ON "ql3"."plugin_package_publisher_revocation_receipts" (publisher, key_id)`,
`
CREATE TABLE "ql3"."plugin_package_publisher_revocation_impacts" (
revocation_receipt_digest char(64) PRIMARY KEY,
impact_digest char(64) NOT NULL,
item_count integer NOT NULL,
generated_at_ms bigint NOT NULL,
impact_json jsonb NOT NULL,
CONSTRAINT ql3_plugin_package_publisher_impact_receipt_fk
FOREIGN KEY (revocation_receipt_digest)
REFERENCES "ql3"."plugin_package_publisher_revocation_receipts"
(receipt_digest)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_publisher_impact_digest_check CHECK (
revocation_receipt_digest ~ '^[0-9a-f]{64}$' AND
impact_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_plugin_package_publisher_impact_count_check CHECK (
item_count BETWEEN 0 AND 4096 AND generated_at_ms >= 0
),
CONSTRAINT ql3_plugin_package_publisher_impact_json_check CHECK (
jsonb_typeof(impact_json) = 'object' AND
octet_length(impact_json::text) BETWEEN 2 AND 8388608 AND
impact_json @> jsonb_build_object(
'schema',
'qinglong/plugin-package-publisher-key-revocation-impact@v1',
'revocationReceiptDigest', revocation_receipt_digest,
'generatedAtMs', generated_at_ms,
'impactDigest', impact_digest
) AND
jsonb_array_length(impact_json -> 'items') = item_count
)
)
`.trim(),
`CREATE UNIQUE INDEX ql3_plugin_package_publisher_impact_digest_key ON "ql3"."plugin_package_publisher_revocation_impacts" (impact_digest)`,
`
CREATE TABLE "ql3"."plugin_package_publisher_revocation_impact_items" (
impact_digest char(64) NOT NULL,
provenance_digest char(64) NOT NULL,
project_id varchar(128) NOT NULL,
package_name varchar(63) NOT NULL,
installation_id varchar(128) NOT NULL,
lock_digest char(64) NOT NULL,
PRIMARY KEY (impact_digest, provenance_digest),
CONSTRAINT ql3_plugin_package_publisher_impact_item_impact_fk
FOREIGN KEY (impact_digest)
REFERENCES "ql3"."plugin_package_publisher_revocation_impacts"
(impact_digest)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_publisher_impact_item_provenance_fk
FOREIGN KEY (provenance_digest)
REFERENCES "ql3"."plugin_package_publisher_provenance"
(provenance_digest)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_publisher_impact_item_install_fk
FOREIGN KEY (installation_id)
REFERENCES "ql3"."plugin_package_installs" (installation_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_publisher_impact_item_identity_check CHECK (
package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$' AND
installation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
impact_digest ~ '^[0-9a-f]{64}$' AND
provenance_digest ~ '^[0-9a-f]{64}$' AND
lock_digest ~ '^[0-9a-f]{64}$'
)
)
`.trim(),
`CREATE UNIQUE INDEX ql3_plugin_package_publisher_impact_item_install_key ON "ql3"."plugin_package_publisher_revocation_impact_items" (impact_digest, installation_id)`,
`CREATE INDEX ql3_plugin_package_publisher_impact_item_target_idx ON "ql3"."plugin_package_publisher_revocation_impact_items" (project_id, package_name, installation_id, lock_digest)`,
`
CREATE FUNCTION "ql3"."enforce_plugin_package_stage_provenance"()
RETURNS trigger
LANGUAGE plpgsql
SET search_path = pg_catalog, ql3
AS $ql3$
BEGIN
IF OLD.state = 'queued' AND NEW.state = 'staged' AND NOT EXISTS (
SELECT 1
FROM "ql3"."plugin_package_publisher_provenance" AS provenance
WHERE provenance.installation_id = NEW.installation_id
AND provenance.project_id = NEW.project_id
AND provenance.package_name = NEW.package_name
AND provenance.lock_digest = NEW.lock_digest
AND provenance.artifact_digest =
NEW.lock_json -> 'source' ->> 'artifactDigest'
AND provenance.manifest_digest = NEW.lock_json ->> 'manifestDigest'
AND provenance.content_digest =
NEW.lock_json -> 'source' ->> 'contentDigest'
AND provenance.stage_evidence_digest =
NEW.record_json -> 'stageReceipt' ->> 'evidenceDigest'
) THEN
RAISE EXCEPTION
'Plugin Package stage requires exact publisher provenance'
USING ERRCODE = 'check_violation';
END IF;
RETURN NEW;
END
$ql3$
`.trim(),
`
CREATE TRIGGER ql3_plugin_package_stage_provenance_guard
BEFORE UPDATE ON "ql3"."plugin_package_installs"
FOR EACH ROW EXECUTE FUNCTION
"ql3"."enforce_plugin_package_stage_provenance"()
`.trim(),
`
CREATE OR REPLACE FUNCTION "ql3"."plugin_package_run_start_allowed"(
p_project_id varchar,
p_task_id varchar,
p_task_revision varchar
)
RETURNS boolean
LANGUAGE plpgsql
VOLATILE
SECURITY DEFINER
SET search_path = pg_catalog, ql3
AS $ql3$
DECLARE
project_exists boolean;
BEGIN
IF NOT pg_has_role(session_user, 'ql3_runtime', 'member') THEN
RAISE EXCEPTION 'Runtime authority is required'
USING ERRCODE = 'insufficient_privilege';
END IF;
SELECT true INTO project_exists
FROM "ql3"."projects"
WHERE id = p_project_id
FOR SHARE;
IF NOT COALESCE(project_exists, false) THEN
RETURN false;
END IF;
RETURN NOT EXISTS (
SELECT 1
FROM "ql3"."plugin_package_task_reconciliations" AS reconciliation
JOIN "ql3"."plugin_package_task_reconciliation_items" AS item
ON item.generation_digest = reconciliation.generation_digest
AND item.task_id = p_task_id
AND 'qltd:v1:' || item.revision || ':' || item.content_digest =
p_task_revision
WHERE reconciliation.project_id = p_project_id
AND (
EXISTS (
SELECT 1
FROM "ql3"."plugin_package_quarantine_events" AS quarantine
WHERE quarantine.project_id = reconciliation.project_id
AND quarantine.package_name = reconciliation.package_name
AND quarantine.lock_digest = reconciliation.lock_digest
) 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.lock_digest = reconciliation.lock_digest
)
)
);
END
$ql3$
`.trim(),
`
CREATE OR REPLACE FUNCTION "ql3"."plugin_package_tool_start_allowed"(
p_project_id varchar,
p_definition_ref varchar,
p_definition_digest char(64)
)
RETURNS boolean
LANGUAGE plpgsql
VOLATILE
SECURITY DEFINER
SET search_path = pg_catalog, ql3
AS $ql3$
DECLARE
project_exists boolean;
BEGIN
IF NOT pg_has_role(session_user, 'ql3_runtime', 'member') THEN
RAISE EXCEPTION 'Runtime authority is required'
USING ERRCODE = 'insufficient_privilege';
END IF;
SELECT true INTO project_exists
FROM "ql3"."projects"
WHERE id = p_project_id
FOR SHARE;
IF NOT COALESCE(project_exists, false) THEN
RETURN false;
END IF;
RETURN NOT EXISTS (
SELECT 1
FROM "ql3"."project_tool_definition_snapshot_sources" AS source
JOIN "ql3"."project_tool_definition_snapshots" AS snapshot
ON snapshot.project_id = source.project_id
AND snapshot.active_vector_digest = source.active_vector_digest
CROSS JOIN LATERAL jsonb_array_elements(
snapshot.snapshot_json -> 'definitions'
) AS definition(item)
WHERE source.project_id = p_project_id
AND definition.item ->> 'packageName' = source.package_name
AND 'tool:' ||
(definition.item -> 'definition' ->> 'name') || '@' ||
(definition.item -> 'definition' ->> 'version') = p_definition_ref
AND definition.item ->> 'definitionDigest' = p_definition_digest
AND (
EXISTS (
SELECT 1
FROM "ql3"."plugin_package_quarantine_events" AS quarantine
WHERE quarantine.project_id = source.project_id
AND quarantine.package_name = source.package_name
AND quarantine.installation_id = source.installation_id
AND quarantine.lock_digest = source.lock_digest
) 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 = source.installation_id
AND provenance.lock_digest = source.lock_digest
)
)
);
END
$ql3$
`.trim(),
`
REVOKE ALL ON
"ql3"."plugin_package_publisher_provenance",
"ql3"."plugin_package_publisher_revocation_receipts",
"ql3"."plugin_package_publisher_revocation_impacts",
"ql3"."plugin_package_publisher_revocation_impact_items"
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_package_executor, ql3_worker_ingress
`.trim(),
`
REVOKE ALL ON FUNCTION
"ql3"."enforce_plugin_package_stage_provenance"()
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_package_executor, ql3_worker_ingress
`.trim(),
`
GRANT SELECT, INSERT ON
"ql3"."plugin_package_publisher_provenance",
"ql3"."plugin_package_publisher_revocation_receipts",
"ql3"."plugin_package_publisher_revocation_impacts",
"ql3"."plugin_package_publisher_revocation_impact_items"
TO ql3_package_executor
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 37,
migration_id = 'pg-0038-plugin-package-publisher-provenance',
capabilities = '${CAPABILITIES_V37}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 36
AND migration_id = 'pg-0037-plugin-package-quarantine'
AND capabilities = '${CAPABILITIES_V36}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 36'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,250 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V37 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V38 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0039PluginPackagePublisherTrustAuthorityMigration =
definePostgresSqlMigration({
id: 'pg-0039-plugin-package-publisher-trust-authority',
statements: [
`
CREATE TABLE "ql3"."plugin_package_publisher_trust_snapshots" (
snapshot_digest char(64) PRIMARY KEY,
key_count integer NOT NULL,
observed_by varchar(128) NOT NULL,
observed_at_ms bigint NOT NULL,
snapshot_json jsonb NOT NULL,
CONSTRAINT ql3_plugin_package_publisher_trust_snapshot_identity_check CHECK (
observed_by ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
),
CONSTRAINT ql3_plugin_package_publisher_trust_snapshot_digest_check CHECK (
snapshot_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_plugin_package_publisher_trust_snapshot_count_check CHECK (
key_count BETWEEN 0 AND 32 AND observed_at_ms >= 0
),
CONSTRAINT ql3_plugin_package_publisher_trust_snapshot_json_check CHECK (
jsonb_typeof(snapshot_json) = 'object' AND
octet_length(snapshot_json::text) BETWEEN 2 AND 262144 AND
snapshot_json @> jsonb_build_object(
'schema', 'qinglong/plugin-package-publisher-trust-snapshot@v1',
'snapshotDigest', snapshot_digest
) AND
jsonb_array_length(snapshot_json -> 'keys') = key_count
)
)
`.trim(),
`
CREATE TABLE "ql3"."plugin_package_publisher_trust_heads" (
authority_id varchar(128) PRIMARY KEY,
generation integer NOT NULL,
base_snapshot_digest char(64) NOT NULL,
effective_trust_digest char(64) NOT NULL,
updated_at_ms bigint NOT NULL,
head_digest char(64) NOT NULL,
head_json jsonb NOT NULL,
CONSTRAINT ql3_plugin_package_publisher_trust_head_snapshot_fk
FOREIGN KEY (base_snapshot_digest)
REFERENCES "ql3"."plugin_package_publisher_trust_snapshots"
(snapshot_digest)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_publisher_trust_head_effective_snapshot_fk
FOREIGN KEY (effective_trust_digest)
REFERENCES "ql3"."plugin_package_publisher_trust_snapshots"
(snapshot_digest)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_publisher_trust_head_identity_check CHECK (
authority_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
generation BETWEEN 1 AND 2147483647
),
CONSTRAINT ql3_plugin_package_publisher_trust_head_digest_check CHECK (
base_snapshot_digest ~ '^[0-9a-f]{64}$' AND
effective_trust_digest ~ '^[0-9a-f]{64}$' AND
head_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_plugin_package_publisher_trust_head_time_check CHECK (
updated_at_ms >= 0
),
CONSTRAINT ql3_plugin_package_publisher_trust_head_json_check CHECK (
jsonb_typeof(head_json) = 'object' AND
octet_length(head_json::text) BETWEEN 2 AND 65536 AND
head_json @> jsonb_build_object(
'schema', 'qinglong/plugin-package-publisher-trust-head@v1',
'authorityId', authority_id,
'generation', generation,
'baseSnapshotDigest', base_snapshot_digest,
'effectiveTrustDigest', effective_trust_digest,
'updatedAtMs', updated_at_ms,
'headDigest', head_digest
)
)
)
`.trim(),
`
CREATE TABLE "ql3"."plugin_package_publisher_revocation_proposals" (
action_ref varchar(255) PRIMARY KEY,
project_id varchar(128) NOT NULL,
authority_id varchar(128) NOT NULL,
trust_generation integer NOT NULL,
publisher varchar(253) NOT NULL,
key_id varchar(128) NOT NULL,
previous_trust_digest char(64) NOT NULL,
current_trust_digest char(64) NOT NULL,
action_type varchar(128) NOT NULL,
permission varchar(128) NOT NULL,
action_digest char(64) NOT NULL,
preview_digest char(64) NOT NULL,
authorization_mode varchar(16) NOT NULL,
reason_code varchar(32) NOT NULL,
proposed_by_type varchar(32) NOT NULL,
proposed_by_id varchar(255) NOT NULL,
proposer_assurance varchar(32) NOT NULL,
fence_project_version integer NOT NULL,
fence_binding_version integer,
created_at_ms bigint NOT NULL,
proposal_json jsonb NOT NULL,
proposal_digest char(64) NOT NULL,
CONSTRAINT ql3_plugin_package_publisher_revocation_proposal_project_fk
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_publisher_revocation_proposal_head_fk
FOREIGN KEY (authority_id)
REFERENCES "ql3"."plugin_package_publisher_trust_heads" (authority_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_publisher_revocation_proposal_identity_check
CHECK (
action_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$' AND
authority_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
publisher ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$' AND
key_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
action_type = 'plugin_package.publisher_key.revoke' AND
permission = 'package.manage' AND
authorization_mode IN ('dual_control', 'break_glass') AND
reason_code IN (
'suspected_key_compromise', 'confirmed_key_compromise'
) AND
proposed_by_type IN (
'user', 'api_app', 'mcp_client', 'agent', 'system', 'worker'
) AND
octet_length(proposed_by_id) BETWEEN 1 AND 255 AND
proposer_assurance IN (
'single_factor', 'multi_factor', 'service', 'hardware',
'local_console'
) AND
(authorization_mode <> 'break_glass' OR
proposer_assurance = 'hardware')
),
CONSTRAINT ql3_plugin_package_publisher_revocation_proposal_digest_check
CHECK (
previous_trust_digest ~ '^[0-9a-f]{64}$' AND
current_trust_digest ~ '^[0-9a-f]{64}$' AND
previous_trust_digest <> current_trust_digest AND
action_digest ~ '^[0-9a-f]{64}$' AND
preview_digest ~ '^[0-9a-f]{64}$' AND
proposal_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_plugin_package_publisher_revocation_proposal_time_check
CHECK (
trust_generation BETWEEN 1 AND 2147483647 AND
fence_project_version BETWEEN 1 AND 2147483647 AND
(fence_binding_version IS NULL OR
fence_binding_version BETWEEN 1 AND 2147483647) AND
created_at_ms >= 0
),
CONSTRAINT ql3_plugin_package_publisher_revocation_proposal_json_check
CHECK (
jsonb_typeof(proposal_json) = 'object' AND
octet_length(proposal_json::text) BETWEEN 2 AND 262144 AND
proposal_json @> jsonb_build_object(
'schema',
'qinglong/plugin-package-publisher-key-revocation-proposal@v1',
'actionRef', action_ref,
'projectId', project_id,
'actionType', action_type,
'permission', permission,
'actionDigest', action_digest,
'previewDigest', preview_digest,
'proposerAssurance', proposer_assurance,
'createdAtMs', created_at_ms,
'proposalDigest', proposal_digest
) AND
proposal_json -> 'actionInput' @> jsonb_build_object(
'authorityProjectId', project_id,
'trustAuthorityId', authority_id,
'trustGeneration', trust_generation,
'publisher', publisher,
'keyId', key_id,
'previousTrustDigest', previous_trust_digest,
'currentTrustDigest', current_trust_digest,
'authorizationMode', authorization_mode,
'reasonCode', reason_code
) AND
proposal_json -> 'proposedBy' @> jsonb_build_object(
'type', proposed_by_type, 'id', proposed_by_id
) AND
proposal_json -> 'proposalFence' @> jsonb_build_object(
'projectVersion', fence_project_version,
'bindingVersion', fence_binding_version
)
)
)
`.trim(),
`CREATE UNIQUE INDEX ql3_plugin_package_publisher_revocation_proposal_digest_key ON "ql3"."plugin_package_publisher_revocation_proposals" (proposal_digest)`,
`CREATE INDEX ql3_plugin_package_publisher_revocation_proposal_project_idx ON "ql3"."plugin_package_publisher_revocation_proposals" (project_id, created_at_ms, action_ref)`,
`CREATE INDEX ql3_plugin_package_publisher_revocation_proposal_signer_idx ON "ql3"."plugin_package_publisher_revocation_proposals" (publisher, key_id, trust_generation)`,
`
REVOKE ALL ON
"ql3"."plugin_package_publisher_trust_snapshots",
"ql3"."plugin_package_publisher_trust_heads",
"ql3"."plugin_package_publisher_revocation_proposals"
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_package_executor, ql3_worker_ingress
`.trim(),
`
GRANT SELECT, INSERT ON
"ql3"."plugin_package_publisher_trust_snapshots",
"ql3"."plugin_package_publisher_trust_heads",
"ql3"."plugin_package_publisher_revocation_proposals"
TO ql3_package_manager
`.trim(),
`
GRANT SELECT, INSERT ON
"ql3"."plugin_package_publisher_trust_snapshots"
TO ql3_package_executor
`.trim(),
`
GRANT SELECT ON
"ql3"."plugin_package_publisher_revocation_proposals"
TO ql3_package_executor
`.trim(),
`
GRANT SELECT, UPDATE ON
"ql3"."plugin_package_publisher_trust_heads"
TO ql3_package_executor
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 38,
migration_id =
'pg-0039-plugin-package-publisher-trust-authority',
capabilities = '${CAPABILITIES_V38}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 37
AND migration_id = 'pg-0038-plugin-package-publisher-provenance'
AND capabilities = '${CAPABILITIES_V37}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 37'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,288 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V38 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V39 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0040PluginPackagePublisherTrustTransitionsMigration =
definePostgresSqlMigration({
id: 'pg-0040-plugin-package-publisher-trust-transitions',
statements: [
`
CREATE TABLE "ql3"."plugin_package_publisher_trust_transition_proposals" (
action_ref varchar(255) PRIMARY KEY,
project_id varchar(128) NOT NULL,
authority_id varchar(128) NOT NULL,
trust_generation integer NOT NULL,
mode varchar(16) NOT NULL,
publisher varchar(253) NOT NULL,
key_id varchar(128) NOT NULL,
previous_trust_digest char(64) NOT NULL,
current_trust_digest char(64) NOT NULL,
action_type varchar(128) NOT NULL,
permission varchar(128) NOT NULL,
action_digest char(64) NOT NULL,
preview_digest char(64) NOT NULL,
proposed_by_type varchar(32) NOT NULL,
proposed_by_id varchar(255) NOT NULL,
proposer_assurance varchar(32) NOT NULL,
fence_project_version integer NOT NULL,
fence_binding_version integer,
created_at_ms bigint NOT NULL,
proposal_json jsonb NOT NULL,
proposal_digest char(64) NOT NULL,
CONSTRAINT ql3_pp_trust_transition_proposal_digest_key
UNIQUE (proposal_digest),
CONSTRAINT ql3_pp_trust_transition_proposal_project_fk
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
ON DELETE RESTRICT,
CONSTRAINT ql3_pp_trust_transition_proposal_head_fk
FOREIGN KEY (authority_id)
REFERENCES "ql3"."plugin_package_publisher_trust_heads" (authority_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_pp_trust_transition_proposal_previous_fk
FOREIGN KEY (previous_trust_digest)
REFERENCES "ql3"."plugin_package_publisher_trust_snapshots"
(snapshot_digest)
ON DELETE RESTRICT,
CONSTRAINT ql3_pp_trust_transition_proposal_current_fk
FOREIGN KEY (current_trust_digest)
REFERENCES "ql3"."plugin_package_publisher_trust_snapshots"
(snapshot_digest)
ON DELETE RESTRICT,
CONSTRAINT ql3_pp_trust_transition_proposal_identity_check
CHECK (
action_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$' AND
authority_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
publisher ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$' AND
key_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
mode IN ('overlap_add', 'safe_retire') AND
action_type = CASE mode
WHEN 'overlap_add'
THEN 'plugin_package.publisher_key.overlap_add'
WHEN 'safe_retire'
THEN 'plugin_package.publisher_key.safe_retire'
END AND
permission = 'package.manage' AND
proposed_by_type = 'user' AND
octet_length(proposed_by_id) BETWEEN 1 AND 255 AND
proposer_assurance IN ('multi_factor', 'hardware')
),
CONSTRAINT ql3_pp_trust_transition_proposal_digest_check
CHECK (
previous_trust_digest ~ '^[0-9a-f]{64}$' AND
current_trust_digest ~ '^[0-9a-f]{64}$' AND
previous_trust_digest <> current_trust_digest AND
action_digest ~ '^[0-9a-f]{64}$' AND
preview_digest ~ '^[0-9a-f]{64}$' AND
proposal_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_pp_trust_transition_proposal_time_check
CHECK (
trust_generation BETWEEN 1 AND 2147483647 AND
fence_project_version BETWEEN 1 AND 2147483647 AND
(fence_binding_version IS NULL OR
fence_binding_version BETWEEN 1 AND 2147483647) AND
created_at_ms >= 0
),
CONSTRAINT ql3_pp_trust_transition_proposal_json_check
CHECK (
jsonb_typeof(proposal_json) = 'object' AND
octet_length(proposal_json::text) BETWEEN 2 AND 262144 AND
proposal_json @> jsonb_build_object(
'schema',
'qinglong/plugin-package-publisher-trust-transition-proposal@v1',
'actionRef', action_ref,
'projectId', project_id,
'actionType', action_type,
'permission', permission,
'actionDigest', action_digest,
'previewDigest', preview_digest,
'proposerAssurance', proposer_assurance,
'createdAtMs', created_at_ms,
'proposalDigest', proposal_digest
) AND
proposal_json -> 'actionInput' @> jsonb_build_object(
'authorityProjectId', project_id,
'trustAuthorityId', authority_id,
'trustGeneration', trust_generation,
'mode', mode,
'publisher', publisher,
'keyId', key_id,
'previousTrustDigest', previous_trust_digest,
'currentTrustDigest', current_trust_digest
) AND
proposal_json -> 'proposedBy' @> jsonb_build_object(
'type', proposed_by_type, 'id', proposed_by_id
) AND
proposal_json -> 'proposalFence' @> jsonb_build_object(
'projectVersion', fence_project_version,
'bindingVersion', fence_binding_version
)
)
)
`.trim(),
`CREATE INDEX ql3_pp_trust_transition_proposal_project_idx ON "ql3"."plugin_package_publisher_trust_transition_proposals" (project_id, created_at_ms, action_ref)`,
`CREATE INDEX ql3_pp_trust_transition_proposal_signer_idx ON "ql3"."plugin_package_publisher_trust_transition_proposals" (publisher, key_id, trust_generation)`,
`
CREATE TABLE "ql3"."plugin_package_publisher_trust_transition_receipts" (
mutation_id varchar(128) PRIMARY KEY,
proposal_digest char(64) NOT NULL,
authority_id varchar(128) NOT NULL,
previous_generation integer NOT NULL,
current_generation integer NOT NULL,
mode varchar(16) NOT NULL,
publisher varchar(253) NOT NULL,
key_id varchar(128) NOT NULL,
previous_trust_digest char(64) NOT NULL,
current_trust_digest char(64) NOT NULL,
proposer_type varchar(32) NOT NULL,
proposer_id varchar(255) NOT NULL,
confirmer_type varchar(32) NOT NULL,
confirmer_id varchar(255) NOT NULL,
retirement_matching_installations integer,
executed_at_ms bigint NOT NULL,
receipt_json jsonb NOT NULL,
receipt_digest char(64) NOT NULL,
CONSTRAINT ql3_pp_trust_transition_receipt_proposal_key
UNIQUE (proposal_digest),
CONSTRAINT ql3_pp_trust_transition_receipt_digest_key
UNIQUE (receipt_digest),
CONSTRAINT ql3_pp_trust_transition_receipt_dispatch_fk
FOREIGN KEY (mutation_id)
REFERENCES "ql3"."approved_action_dispatches" (dispatch_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_pp_trust_transition_receipt_proposal_fk
FOREIGN KEY (proposal_digest)
REFERENCES "ql3"."plugin_package_publisher_trust_transition_proposals"
(proposal_digest)
ON DELETE RESTRICT,
CONSTRAINT ql3_pp_trust_transition_receipt_head_fk
FOREIGN KEY (authority_id)
REFERENCES "ql3"."plugin_package_publisher_trust_heads" (authority_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_pp_trust_transition_receipt_previous_fk
FOREIGN KEY (previous_trust_digest)
REFERENCES "ql3"."plugin_package_publisher_trust_snapshots"
(snapshot_digest)
ON DELETE RESTRICT,
CONSTRAINT ql3_pp_trust_transition_receipt_current_fk
FOREIGN KEY (current_trust_digest)
REFERENCES "ql3"."plugin_package_publisher_trust_snapshots"
(snapshot_digest)
ON DELETE RESTRICT,
CONSTRAINT ql3_pp_trust_transition_receipt_identity_check
CHECK (
mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
proposal_digest ~ '^[0-9a-f]{64}$' AND
authority_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
mode IN ('overlap_add', 'safe_retire') AND
publisher ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$' AND
key_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
proposer_type = 'user' AND
confirmer_type = 'user' AND
octet_length(proposer_id) BETWEEN 1 AND 255 AND
octet_length(confirmer_id) BETWEEN 1 AND 255 AND
(proposer_type, proposer_id) <> (confirmer_type, confirmer_id)
),
CONSTRAINT ql3_pp_trust_transition_receipt_transition_check
CHECK (
previous_generation BETWEEN 1 AND 2147483646 AND
current_generation = previous_generation + 1 AND
previous_trust_digest ~ '^[0-9a-f]{64}$' AND
current_trust_digest ~ '^[0-9a-f]{64}$' AND
previous_trust_digest <> current_trust_digest AND
(
(mode = 'overlap_add' AND
retirement_matching_installations IS NULL) OR
(mode = 'safe_retire' AND
retirement_matching_installations = 0)
) AND
executed_at_ms >= 0 AND
receipt_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_pp_trust_transition_receipt_json_check
CHECK (
jsonb_typeof(receipt_json) = 'object' AND
octet_length(receipt_json::text) BETWEEN 2 AND 262144 AND
receipt_json @> jsonb_build_object(
'schema',
'qinglong/plugin-package-publisher-trust-transition-receipt@v1',
'mutationId', mutation_id,
'proposalDigest', proposal_digest,
'trustAuthorityId', authority_id,
'previousGeneration', previous_generation,
'currentGeneration', current_generation,
'mode', mode,
'publisher', publisher,
'keyId', key_id,
'previousTrustDigest', previous_trust_digest,
'currentTrustDigest', current_trust_digest,
'retirementMatchingInstallations',
retirement_matching_installations,
'executedAtMs', executed_at_ms,
'receiptDigest', receipt_digest
) AND
receipt_json -> 'proposer' @> jsonb_build_object(
'type', proposer_type, 'id', proposer_id
) AND
receipt_json -> 'confirmer' @> jsonb_build_object(
'type', confirmer_type, 'id', confirmer_id
)
)
)
`.trim(),
`CREATE INDEX ql3_pp_trust_transition_receipt_signer_idx ON "ql3"."plugin_package_publisher_trust_transition_receipts" (publisher, key_id, current_generation)`,
`
REVOKE ALL ON
"ql3"."plugin_package_publisher_trust_transition_proposals",
"ql3"."plugin_package_publisher_trust_transition_receipts"
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_package_executor, ql3_worker_ingress
`.trim(),
`
GRANT SELECT, INSERT ON
"ql3"."plugin_package_publisher_trust_transition_proposals"
TO ql3_package_manager
`.trim(),
`
GRANT SELECT ON
"ql3"."plugin_package_publisher_trust_transition_receipts"
TO ql3_package_manager
`.trim(),
`
GRANT SELECT ON
"ql3"."plugin_package_publisher_trust_transition_proposals"
TO ql3_package_executor
`.trim(),
`
GRANT SELECT, INSERT ON
"ql3"."plugin_package_publisher_trust_transition_receipts"
TO ql3_package_executor
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 39,
migration_id =
'pg-0040-plugin-package-publisher-trust-transitions',
capabilities = '${CAPABILITIES_V39}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 38
AND migration_id =
'pg-0039-plugin-package-publisher-trust-authority'
AND capabilities = '${CAPABILITIES_V38}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 38'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,108 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V40 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V41 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0042PluginPackageLifecyclePlansMigration =
definePostgresSqlMigration({
id: 'pg-0042-plugin-package-lifecycle-plans',
statements: [
`
CREATE TABLE "ql3"."plugin_package_lifecycle_plans" (
action_ref varchar(255) PRIMARY KEY,
plan_digest char(64) NOT NULL,
action varchar(16) NOT NULL,
project_id varchar(128) NOT NULL,
package_name varchar(63) NOT NULL,
installation_id varchar(128) NOT NULL,
lock_digest char(64) NOT NULL,
impact_digest char(64) NOT NULL,
requested_by_type varchar(16) NOT NULL,
requested_by_id varchar(255) NOT NULL,
planned_at_ms bigint NOT NULL,
expires_at_ms bigint NOT NULL,
plan_json jsonb NOT NULL,
CONSTRAINT ql3_plugin_package_lifecycle_plan_install_fk
FOREIGN KEY (installation_id)
REFERENCES "ql3"."plugin_package_installs" (installation_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_lifecycle_plan_identity_check CHECK (
action_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$'
AND action IN ('disable', 'enable', 'uninstall')
AND package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$'
AND requested_by_type = 'user'
AND char_length(requested_by_id) BETWEEN 1 AND 255
AND requested_by_id !~ '[[:cntrl:]]'
),
CONSTRAINT ql3_plugin_package_lifecycle_plan_digest_check CHECK (
plan_digest ~ '^[0-9a-f]{64}$'
AND lock_digest ~ '^[0-9a-f]{64}$'
AND impact_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_plugin_package_lifecycle_plan_time_check CHECK (
planned_at_ms >= 0
AND expires_at_ms > planned_at_ms
AND expires_at_ms - planned_at_ms <= 900000
),
CONSTRAINT ql3_plugin_package_lifecycle_plan_json_check CHECK (
jsonb_typeof(plan_json) = 'object'
AND octet_length(plan_json::text) BETWEEN 2 AND 98304
AND plan_json @> jsonb_build_object(
'schema', 'qinglong/plugin-package-lifecycle-plan@v1',
'actionRef', action_ref,
'planDigest', plan_digest,
'requestedBy', jsonb_build_object(
'type', requested_by_type,
'id', requested_by_id
),
'plannedAtMs', planned_at_ms,
'expiresAtMs', expires_at_ms,
'impact', jsonb_build_object(
'action', action,
'impactDigest', impact_digest,
'target', jsonb_build_object(
'projectId', project_id,
'packageName', package_name,
'installationId', installation_id,
'lockDigest', lock_digest
)
)
)
)
)
`.trim(),
`CREATE UNIQUE INDEX ql3_plugin_package_lifecycle_plan_digest_key ON "ql3"."plugin_package_lifecycle_plans" (plan_digest)`,
`CREATE UNIQUE INDEX ql3_plugin_package_lifecycle_plan_impact_key ON "ql3"."plugin_package_lifecycle_plans" (project_id, package_name, installation_id, lock_digest, impact_digest)`,
`CREATE INDEX ql3_plugin_package_lifecycle_plan_expiry_idx ON "ql3"."plugin_package_lifecycle_plans" (expires_at_ms, action_ref)`,
`
REVOKE ALL ON "ql3"."plugin_package_lifecycle_plans"
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_package_executor, ql3_worker_ingress
`.trim(),
`GRANT SELECT ON "ql3"."plugin_package_lifecycle_plans" TO ql3_package_manager, ql3_package_executor`,
`GRANT INSERT ON "ql3"."plugin_package_lifecycle_plans" TO ql3_package_executor`,
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 41,
migration_id = 'pg-0042-plugin-package-lifecycle-plans',
capabilities = '${CAPABILITIES_V41}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 40
AND migration_id = 'pg-0041-plugin-package-lifecycle'
AND capabilities = '${CAPABILITIES_V40}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 40'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,181 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V41 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V42 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0043PluginPackageAutomationPublicationsMigration =
definePostgresSqlMigration({
id: 'pg-0043-plugin-package-automation-publications',
statements: [
`
CREATE TABLE "ql3"."plugin_package_automation_publications" (
publication_digest char(64) PRIMARY KEY,
project_id varchar(128) NOT NULL,
package_name varchar(63) NOT NULL,
installation_id varchar(128) NOT NULL,
lock_digest char(64) NOT NULL,
generation integer NOT NULL,
generation_digest char(64) NOT NULL,
materialized_revision_digest char(64) NOT NULL,
state varchar(16) NOT NULL,
version integer NOT NULL,
previous_publication_digest char(64),
lifecycle_event_digest char(64),
published_at_ms bigint NOT NULL,
publication_json jsonb NOT NULL,
CONSTRAINT ql3_plugin_package_automation_publication_revision_fk
FOREIGN KEY (generation_digest)
REFERENCES "ql3"."plugin_package_materialized_revisions" (
generation_digest
) ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_automation_publication_previous_fk
FOREIGN KEY (previous_publication_digest)
REFERENCES "ql3"."plugin_package_automation_publications" (
publication_digest
) ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_automation_publication_lifecycle_fk
FOREIGN KEY (lifecycle_event_digest)
REFERENCES "ql3"."plugin_package_lifecycle_events" (event_digest)
ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_automation_publication_identity_check CHECK (
package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$' 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 (
publication_digest ~ '^[0-9a-f]{64}$' AND
lock_digest ~ '^[0-9a-f]{64}$' AND
generation_digest ~ '^[0-9a-f]{64}$' AND
materialized_revision_digest ~ '^[0-9a-f]{64}$' AND
(
previous_publication_digest IS NULL OR
previous_publication_digest ~ '^[0-9a-f]{64}$'
) AND
(
lifecycle_event_digest IS NULL OR
lifecycle_event_digest ~ '^[0-9a-f]{64}$'
)
),
CONSTRAINT ql3_plugin_package_automation_publication_json_check CHECK (
jsonb_typeof(publication_json) = 'object' AND
octet_length(publication_json::text) BETWEEN 2 AND 12582912 AND
publication_json @> jsonb_build_object(
'schema', 'qinglong/plugin-package-automation-publication@v1',
'state', state,
'version', version,
'previousPublicationDigest', previous_publication_digest,
'lifecycleEventDigest', lifecycle_event_digest,
'publishedAtMs', published_at_ms,
'publicationDigest', publication_digest,
'target', jsonb_build_object(
'projectId', project_id,
'packageName', package_name,
'installationId', installation_id,
'lockDigest', lock_digest,
'generation', generation,
'generationDigest', generation_digest,
'materializedRevisionDigest', materialized_revision_digest
)
) AND
jsonb_typeof(publication_json -> 'definitions' -> 'workflows') =
'array' AND
jsonb_typeof(publication_json -> 'definitions' -> 'prompts') =
'array' AND
(
state = 'absent' AND
jsonb_array_length(publication_json -> 'definitions' -> 'workflows') +
jsonb_array_length(publication_json -> 'definitions' -> 'prompts') = 0
OR
state <> 'absent' AND
jsonb_array_length(publication_json -> 'definitions' -> 'workflows') +
jsonb_array_length(publication_json -> 'definitions' -> 'prompts') > 0
)
)
)
`.trim(),
`CREATE UNIQUE INDEX ql3_plugin_package_automation_publication_version_key ON "ql3"."plugin_package_automation_publications" (project_id, package_name, version)`,
`CREATE UNIQUE INDEX ql3_plugin_package_automation_publication_previous_key ON "ql3"."plugin_package_automation_publications" (previous_publication_digest) WHERE previous_publication_digest IS NOT NULL`,
`CREATE INDEX ql3_plugin_package_automation_publication_generation_idx ON "ql3"."plugin_package_automation_publications" (generation_digest, publication_digest)`,
`
CREATE TABLE "ql3"."plugin_package_automation_publication_heads" (
project_id varchar(128) NOT NULL,
package_name varchar(63) NOT NULL,
publication_digest char(64) NOT NULL,
generation_digest char(64) NOT NULL,
state varchar(16) NOT NULL,
version integer NOT NULL,
updated_at_ms bigint NOT NULL,
PRIMARY KEY (project_id, package_name),
CONSTRAINT ql3_plugin_package_automation_publication_head_publication_fk
FOREIGN KEY (publication_digest)
REFERENCES "ql3"."plugin_package_automation_publications" (
publication_digest
) ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_automation_publication_head_state_check CHECK (
publication_digest ~ '^[0-9a-f]{64}$' AND
generation_digest ~ '^[0-9a-f]{64}$' AND
state IN ('active', 'withdrawn', 'absent') AND
version BETWEEN 1 AND 2147483647 AND
updated_at_ms >= 0
)
)
`.trim(),
`CREATE UNIQUE INDEX ql3_plugin_package_automation_publication_head_digest_key ON "ql3"."plugin_package_automation_publication_heads" (publication_digest)`,
`
REVOKE ALL
ON "ql3"."plugin_package_automation_publications",
"ql3"."plugin_package_automation_publication_heads"
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_package_executor, ql3_worker_ingress
`.trim(),
`
GRANT SELECT
ON "ql3"."plugin_package_automation_publications",
"ql3"."plugin_package_automation_publication_heads"
TO ql3_runtime, ql3_package_manager, ql3_package_executor
`.trim(),
`
GRANT INSERT
ON "ql3"."plugin_package_automation_publications"
TO ql3_package_executor
`.trim(),
`
GRANT INSERT, UPDATE
ON "ql3"."plugin_package_automation_publication_heads"
TO ql3_package_executor
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 42,
migration_id = 'pg-0043-plugin-package-automation-publications',
capabilities = '${CAPABILITIES_V42}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 41
AND migration_id = 'pg-0042-plugin-package-lifecycle-plans'
AND capabilities = '${CAPABILITIES_V41}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 41'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,160 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V42 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V43 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0044PluginPackageAutomationStartGuardMigration =
definePostgresSqlMigration({
id: 'pg-0044-plugin-package-automation-start-guard',
statements: [
`
CREATE FUNCTION "ql3"."plugin_package_automation_start_allowed"(
p_project_id varchar,
p_package_name varchar,
p_publication_digest char(64)
)
RETURNS boolean
LANGUAGE plpgsql
VOLATILE
SECURITY DEFINER
SET search_path = pg_catalog, ql3
AS $ql3$
DECLARE
project_exists boolean;
current_publication "ql3"."plugin_package_automation_publications"%ROWTYPE;
current_lifecycle varchar(16);
current_installation_id varchar(128);
current_publisher varchar(253);
current_key_id varchar(128);
BEGIN
IF NOT pg_has_role(session_user, 'ql3_runtime', 'member') THEN
RAISE EXCEPTION 'Runtime authority is required'
USING ERRCODE = 'insufficient_privilege';
END IF;
SELECT true INTO project_exists
FROM "ql3"."projects"
WHERE id = p_project_id
FOR SHARE;
IF NOT COALESCE(project_exists, false) THEN
RETURN false;
END IF;
SELECT publication.* INTO current_publication
FROM "ql3"."plugin_package_automation_publication_heads" AS head
JOIN "ql3"."plugin_package_automation_publications" AS publication
ON publication.publication_digest = head.publication_digest
WHERE head.project_id = p_project_id
AND head.package_name = p_package_name
AND head.publication_digest = p_publication_digest
FOR SHARE OF head, publication;
IF NOT FOUND OR current_publication.state <> 'active' THEN
RETURN false;
END IF;
SELECT install.installation_id INTO current_installation_id
FROM "ql3"."plugin_package_install_heads" AS head
JOIN "ql3"."plugin_package_installs" AS install
ON install.installation_id = head.installation_id
WHERE head.project_id = current_publication.project_id
AND head.package_name = current_publication.package_name
AND head.installation_id = current_publication.installation_id
AND install.lock_digest = current_publication.lock_digest
AND install.state = 'active'
AND install.active_lock_digest = current_publication.lock_digest
FOR SHARE OF head, install;
IF NOT FOUND THEN
RETURN false;
END IF;
SELECT disposition INTO current_lifecycle
FROM "ql3"."plugin_package_lifecycle_heads"
WHERE project_id = current_publication.project_id
AND package_name = current_publication.package_name
FOR SHARE;
IF FOUND AND current_lifecycle <> 'active' THEN
RETURN false;
END IF;
IF EXISTS (
SELECT 1
FROM "ql3"."plugin_package_quarantine_events" AS quarantine
WHERE quarantine.project_id = current_publication.project_id
AND quarantine.package_name = current_publication.package_name
AND quarantine.installation_id =
current_publication.installation_id
AND quarantine.lock_digest = current_publication.lock_digest
) THEN
RETURN false;
END IF;
SELECT provenance.publisher, provenance.key_id
INTO current_publisher, current_key_id
FROM "ql3"."plugin_package_publisher_provenance" AS provenance
WHERE provenance.installation_id = current_publication.installation_id
AND provenance.lock_digest = current_publication.lock_digest;
IF NOT FOUND THEN
RETURN false;
END IF;
PERFORM pg_advisory_xact_lock(
hashtextextended(
octet_length(current_publisher)::text || ':' ||
current_publisher ||
octet_length(current_key_id)::text || ':' || current_key_id,
774635230
)
);
IF EXISTS (
SELECT 1
FROM "ql3"."plugin_package_publisher_revocation_receipts" AS revoked
WHERE revoked.publisher = current_publisher
AND revoked.key_id = current_key_id
) THEN
RETURN false;
END IF;
RETURN true;
END
$ql3$
`.trim(),
`
REVOKE ALL ON FUNCTION
"ql3"."plugin_package_automation_start_allowed"(
varchar, varchar, char(64)
)
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_package_executor, ql3_worker_ingress
`.trim(),
`
GRANT EXECUTE ON FUNCTION
"ql3"."plugin_package_automation_start_allowed"(
varchar, varchar, char(64)
)
TO ql3_runtime
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 43,
migration_id = 'pg-0044-plugin-package-automation-start-guard',
capabilities = '${CAPABILITIES_V43}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 42
AND migration_id = 'pg-0043-plugin-package-automation-publications'
AND capabilities = '${CAPABILITIES_V42}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 42'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,234 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V43 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V44 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"plugin_package_workflow_admission":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0045PluginPackageWorkflowAdmissionsMigration =
definePostgresSqlMigration({
id: 'pg-0045-plugin-package-workflow-admissions',
statements: [
`
CREATE TABLE "ql3"."plugin_package_workflow_admissions" (
plan_digest char(64) PRIMARY KEY,
plan_id varchar(128) NOT NULL,
run_id varchar(36) NOT NULL,
project_id varchar(128) NOT NULL,
package_name varchar(63) NOT NULL,
installation_id varchar(128) NOT NULL,
lock_digest char(64) NOT NULL,
generation integer NOT NULL,
generation_digest char(64) NOT NULL,
materialized_revision_digest char(64) NOT NULL,
publication_digest char(64) NOT NULL,
workflow_id varchar(63) NOT NULL,
workflow_definition_digest char(64) NOT NULL,
step_count integer NOT NULL,
admitted_at_ms bigint NOT NULL,
final_run_version integer NOT NULL,
final_run_event_sequence integer NOT NULL,
receipt_digest char(64) NOT NULL,
plan_json jsonb NOT NULL,
receipt_json jsonb NOT NULL,
CONSTRAINT plugin_package_workflow_admissions_plan_id_key
UNIQUE (plan_id),
CONSTRAINT plugin_package_workflow_admissions_run_id_key
UNIQUE (run_id),
CONSTRAINT plugin_package_workflow_admissions_receipt_digest_key
UNIQUE (receipt_digest),
CONSTRAINT ql3_plugin_package_workflow_admission_run_fk
FOREIGN KEY (run_id) REFERENCES "ql3"."runs" (id) ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_workflow_admission_publication_fk
FOREIGN KEY (publication_digest)
REFERENCES "ql3"."plugin_package_automation_publications" (
publication_digest
) ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_workflow_admission_identity_check CHECK (
plan_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,35}$' AND
package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$' AND
workflow_id ~ '^[a-z][a-z0-9-]{0,62}$' AND
generation BETWEEN 1 AND 2147483647 AND
step_count BETWEEN 1 AND 128 AND
admitted_at_ms >= 0 AND
final_run_version = step_count + 1 AND
final_run_event_sequence = step_count + 1
),
CONSTRAINT ql3_plugin_package_workflow_admission_digest_check CHECK (
plan_digest ~ '^[0-9a-f]{64}$' AND
lock_digest ~ '^[0-9a-f]{64}$' AND
generation_digest ~ '^[0-9a-f]{64}$' AND
materialized_revision_digest ~ '^[0-9a-f]{64}$' AND
publication_digest ~ '^[0-9a-f]{64}$' AND
workflow_definition_digest ~ '^[0-9a-f]{64}$' AND
receipt_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_plugin_package_workflow_admission_json_check CHECK (
jsonb_typeof(plan_json) = 'object' AND
octet_length(plan_json::text) BETWEEN 2 AND 262144 AND
plan_json @> jsonb_build_object(
'schema', 'qinglong/plugin-package-workflow-execution-plan@v1',
'planId', plan_id, 'planDigest', plan_digest, 'runId', run_id,
'plannedAtMs', admitted_at_ms
) AND
plan_json -> 'target' @> jsonb_build_object(
'projectId', project_id, 'packageName', package_name,
'installationId', installation_id, 'lockDigest', lock_digest,
'generation', generation, 'generationDigest', generation_digest,
'materializedRevisionDigest', materialized_revision_digest,
'publicationDigest', publication_digest, 'workflowId', workflow_id,
'workflowDefinitionDigest', workflow_definition_digest
) AND
jsonb_typeof(plan_json -> 'steps') = 'array' AND
jsonb_array_length(plan_json -> 'steps') = step_count AND
jsonb_typeof(receipt_json) = 'object' AND
octet_length(receipt_json::text) BETWEEN 2 AND 262144 AND
receipt_json @> jsonb_build_object(
'schema', 'qinglong/plugin-package-workflow-admission-receipt@v1',
'planId', plan_id, 'planDigest', plan_digest, 'runId', run_id,
'publicationDigest', publication_digest, 'workflowId', workflow_id,
'finalRunVersion', final_run_version,
'finalRunEventSequence', final_run_event_sequence,
'admittedAtMs', admitted_at_ms, 'receiptDigest', receipt_digest
) AND
jsonb_typeof(receipt_json -> 'steps') = 'array' AND
jsonb_array_length(receipt_json -> 'steps') = step_count
)
)
`.trim(),
`CREATE UNIQUE INDEX ql3_plugin_package_workflow_admission_plan_run_key ON "ql3"."plugin_package_workflow_admissions" (plan_digest, run_id)`,
`CREATE INDEX ql3_plugin_package_workflow_admission_target_idx ON "ql3"."plugin_package_workflow_admissions" (project_id, package_name, admitted_at_ms, plan_digest)`,
`
CREATE TABLE "ql3"."plugin_package_workflow_admission_steps" (
plan_digest char(64) NOT NULL,
run_id varchar(36) NOT NULL,
step_key varchar(63) NOT NULL,
step_run_id varchar(128) NOT NULL,
task_id varchar(63) NOT NULL,
task_definition_ref varchar(512) NOT NULL,
task_definition_digest char(64) NOT NULL,
needs_json jsonb NOT NULL,
initial_status varchar(16) NOT NULL,
mutation_id varchar(128) NOT NULL,
event_id varchar(36) NOT NULL,
PRIMARY KEY (plan_digest, step_key),
CONSTRAINT plugin_package_workflow_admission_steps_step_run_id_key
UNIQUE (step_run_id),
CONSTRAINT plugin_package_workflow_admission_steps_mutation_id_key
UNIQUE (mutation_id),
CONSTRAINT plugin_package_workflow_admission_steps_event_id_key
UNIQUE (event_id),
CONSTRAINT ql3_plugin_package_workflow_admission_step_admission_fk
FOREIGN KEY (plan_digest, run_id)
REFERENCES "ql3"."plugin_package_workflow_admissions" (
plan_digest, run_id
) ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_workflow_admission_step_run_fk
FOREIGN KEY (run_id, step_run_id)
REFERENCES "ql3"."step_runs" (run_id, id) ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_workflow_admission_step_mutation_fk
FOREIGN KEY (mutation_id)
REFERENCES "ql3"."step_run_mutations" (mutation_id) ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_workflow_admission_step_event_fk
FOREIGN KEY (event_id)
REFERENCES "ql3"."run_events" (id) ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_workflow_admission_step_identity_check CHECK (
step_key ~ '^[a-z][a-z0-9-]{0,62}$' AND
task_id ~ '^[a-z][a-z0-9-]{0,62}$' AND
initial_status IN ('pending', 'ready') AND
task_definition_digest ~ '^[0-9a-f]{64}$' AND
jsonb_typeof(needs_json) = 'array' AND
jsonb_array_length(needs_json) BETWEEN 0 AND 127
)
)
`.trim(),
`CREATE INDEX ql3_plugin_package_workflow_admission_step_task_idx ON "ql3"."plugin_package_workflow_admission_steps" (task_id, task_definition_digest, plan_digest)`,
`
CREATE FUNCTION "ql3"."plugin_package_workflow_admission_snapshot"(
p_project_id varchar,
p_package_name varchar,
p_publication_digest char(64)
)
RETURNS TABLE(publication_json jsonb, revision_json jsonb)
LANGUAGE plpgsql
VOLATILE
SECURITY DEFINER
SET search_path = pg_catalog, ql3
AS $ql3$
BEGIN
IF NOT pg_has_role(session_user, 'ql3_runtime', 'member') THEN
RAISE EXCEPTION 'Runtime authority is required'
USING ERRCODE = 'insufficient_privilege';
END IF;
IF NOT "ql3"."plugin_package_automation_start_allowed"(
p_project_id, p_package_name, p_publication_digest
) THEN
RETURN;
END IF;
RETURN QUERY
SELECT publication.publication_json, revision.revision_json
FROM "ql3"."plugin_package_automation_publications" AS publication
JOIN "ql3"."plugin_package_materialized_revisions" AS revision
ON revision.generation_digest = publication.generation_digest
AND revision.revision_digest =
publication.materialized_revision_digest
WHERE publication.project_id = p_project_id
AND publication.package_name = p_package_name
AND publication.publication_digest = p_publication_digest
AND publication.state = 'active'
FOR SHARE OF publication, revision;
END
$ql3$
`.trim(),
`
REVOKE ALL
ON "ql3"."plugin_package_workflow_admissions",
"ql3"."plugin_package_workflow_admission_steps"
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_package_executor, ql3_worker_ingress
`.trim(),
`
GRANT SELECT, INSERT
ON "ql3"."plugin_package_workflow_admissions",
"ql3"."plugin_package_workflow_admission_steps"
TO ql3_runtime
`.trim(),
`
REVOKE ALL ON FUNCTION
"ql3"."plugin_package_workflow_admission_snapshot"(
varchar, varchar, char(64)
)
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_package_executor, ql3_worker_ingress
`.trim(),
`
GRANT EXECUTE ON FUNCTION
"ql3"."plugin_package_workflow_admission_snapshot"(
varchar, varchar, char(64)
)
TO ql3_runtime
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 44,
migration_id = 'pg-0045-plugin-package-workflow-admissions',
capabilities = '${CAPABILITIES_V44}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 43
AND migration_id = 'pg-0044-plugin-package-automation-start-guard'
AND capabilities = '${CAPABILITIES_V43}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 43'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,258 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V44 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"plugin_package_workflow_admission":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V45 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_task_attempt_admission":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0046PluginPackageWorkflowTaskAttemptAdmissionsMigration =
definePostgresSqlMigration({
id: 'pg-0046-plugin-package-workflow-task-attempt-admissions',
statements: [
`
CREATE UNIQUE INDEX ql3_plugin_package_task_reconciliation_receipt_uidx
ON "ql3"."plugin_package_task_reconciliations"
(generation_digest, receipt_digest)
`.trim(),
`
CREATE TABLE "ql3"."plugin_package_workflow_task_attempt_admissions" (
receipt_digest char(64) PRIMARY KEY,
attempt_id varchar(36) NOT NULL,
plan_digest char(64) NOT NULL,
run_id varchar(36) NOT NULL,
step_run_id varchar(128) NOT NULL,
step_run_version integer NOT NULL,
step_run_digest char(64) NOT NULL,
generation_digest char(64) NOT NULL,
resource_task_id varchar(128) NOT NULL,
task_reconciliation_receipt_digest char(64) NOT NULL,
project_id varchar(128) NOT NULL,
task_id varchar(128) NOT NULL,
source_revision integer NOT NULL,
task_revision varchar(96) NOT NULL,
task_definition_digest char(64) NOT NULL,
executor_type varchar(32) NOT NULL,
execution_digest char(64) NOT NULL,
attempt_number integer NOT NULL,
event_id varchar(36) NOT NULL,
run_version integer NOT NULL,
run_event_sequence integer NOT NULL,
admitted_at_ms bigint NOT NULL,
receipt_json jsonb NOT NULL,
CONSTRAINT plugin_package_workflow_task_attempt_admissions_attempt_id_key
UNIQUE (attempt_id),
CONSTRAINT plugin_package_workflow_task_attempt_admissions_event_id_key
UNIQUE (event_id),
CONSTRAINT plugin_package_workflow_task_attempt_admissions_epoch_key
UNIQUE (run_id, step_run_id, step_run_version),
CONSTRAINT plugin_package_workflow_task_attempt_admissions_number_key
UNIQUE (run_id, attempt_number),
CONSTRAINT ql3_plugin_package_workflow_task_attempt_admission_plan_fk
FOREIGN KEY (plan_digest, run_id)
REFERENCES "ql3"."plugin_package_workflow_admissions" (
plan_digest, run_id
) ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_workflow_task_attempt_admission_step_fk
FOREIGN KEY (run_id, step_run_id)
REFERENCES "ql3"."step_runs" (run_id, id) ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_workflow_task_attempt_admission_attempt_fk
FOREIGN KEY (attempt_id)
REFERENCES "ql3"."run_attempts" (id) ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_workflow_task_attempt_admission_event_fk
FOREIGN KEY (event_id)
REFERENCES "ql3"."run_events" (id) ON DELETE RESTRICT,
CONSTRAINT ql3_pp_workflow_task_attempt_reconciliation_fk
FOREIGN KEY (
generation_digest, task_reconciliation_receipt_digest
) REFERENCES "ql3"."plugin_package_task_reconciliations" (
generation_digest, receipt_digest
) ON DELETE RESTRICT,
CONSTRAINT ql3_plugin_package_workflow_task_attempt_admission_execution_fk
FOREIGN KEY (
project_id, task_id, source_revision, executor_type
) REFERENCES "ql3"."task_execution_revisions" (
project_id, task_id, source_revision, executor_type
) ON DELETE RESTRICT,
CONSTRAINT ql3_pp_workflow_task_attempt_identity_check
CHECK (
attempt_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,35}$' AND
run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,35}$' AND
char_length(step_run_id) BETWEEN 1 AND 128 AND
char_length(resource_task_id) BETWEEN 1 AND 128 AND
char_length(project_id) BETWEEN 1 AND 128 AND
char_length(task_id) BETWEEN 1 AND 128 AND
event_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,35}$' AND
executor_type = 'remote_worker'
),
CONSTRAINT ql3_pp_workflow_task_attempt_counter_check
CHECK (
step_run_version BETWEEN 1 AND 2147483647 AND
source_revision BETWEEN 1 AND 2147483647 AND
attempt_number BETWEEN 1 AND 8192 AND
run_version BETWEEN 1 AND 2147483647 AND
run_event_sequence = run_version AND
admitted_at_ms >= 0
),
CONSTRAINT
ql3_plugin_package_workflow_task_attempt_admission_digest_check
CHECK (
receipt_digest ~ '^[0-9a-f]{64}$' AND
plan_digest ~ '^[0-9a-f]{64}$' AND
step_run_digest ~ '^[0-9a-f]{64}$' AND
generation_digest ~ '^[0-9a-f]{64}$' AND
task_reconciliation_receipt_digest ~ '^[0-9a-f]{64}$' AND
task_definition_digest ~ '^[0-9a-f]{64}$' AND
execution_digest ~ '^[0-9a-f]{64}$' AND
task_revision = concat(
'qltd:v1:', source_revision::text, ':', task_definition_digest
)
),
CONSTRAINT
ql3_plugin_package_workflow_task_attempt_admission_json_check
CHECK (
jsonb_typeof(receipt_json) = 'object' AND
octet_length(receipt_json::text) BETWEEN 2 AND 16384 AND
receipt_json @> jsonb_build_object(
'schema',
'qinglong/plugin-package-workflow-task-attempt-admission@v1',
'receiptDigest', receipt_digest,
'attemptId', attempt_id,
'planDigest', plan_digest,
'runId', run_id,
'stepRunId', step_run_id,
'stepRunVersion', step_run_version,
'stepRunDigest', step_run_digest,
'resourceTaskId', resource_task_id,
'taskReconciliationReceiptDigest',
task_reconciliation_receipt_digest,
'taskId', task_id,
'taskRevision', task_revision,
'taskDefinitionDigest', task_definition_digest,
'executorType', executor_type,
'executionDigest', execution_digest,
'attemptNumber', attempt_number,
'eventId', event_id,
'runVersion', run_version,
'runEventSequence', run_event_sequence,
'admittedAtMs', admitted_at_ms
)
)
)
`.trim(),
`
CREATE INDEX ql3_pp_workflow_task_attempt_candidate_idx
ON "ql3"."plugin_package_workflow_task_attempt_admissions"
(run_id, step_run_id, admitted_at_ms)
`.trim(),
`
CREATE FUNCTION "ql3"."plugin_package_workflow_task_attempt_snapshot"(
p_run_id varchar,
p_step_run_id varchar
)
RETURNS TABLE(
plan_json jsonb,
reconciliation_json jsonb,
execution_project_id varchar,
execution_task_id varchar,
execution_source_revision integer,
execution_task_revision varchar,
execution_source_content_digest char(64),
execution_executor_type varchar,
execution_plan_schema varchar,
execution_plan_json jsonb,
execution_content_digest char(64),
execution_created_at_ms bigint
)
LANGUAGE plpgsql
VOLATILE
SECURITY DEFINER
SET search_path = pg_catalog, ql3
AS $ql3$
BEGIN
IF NOT pg_has_role(session_user, 'ql3_runtime', 'member') THEN
RAISE EXCEPTION 'Runtime authority is required'
USING ERRCODE = 'insufficient_privilege';
END IF;
RETURN QUERY
SELECT
workflow.plan_json,
reconciliation.receipt_json,
execution.project_id,
execution.task_id,
execution.source_revision,
execution.task_revision,
execution.source_content_digest,
execution.executor_type,
execution.plan_schema,
execution.plan_json,
execution.content_digest,
execution.created_at_ms
FROM "ql3"."plugin_package_workflow_admissions" AS workflow
JOIN "ql3"."plugin_package_workflow_admission_steps" AS source
ON source.plan_digest = workflow.plan_digest
AND source.run_id = workflow.run_id
AND source.step_run_id = p_step_run_id
JOIN "ql3"."plugin_package_task_reconciliations" AS reconciliation
ON reconciliation.generation_digest = workflow.generation_digest
JOIN "ql3"."plugin_package_task_reconciliation_items" AS item
ON item.generation_digest = reconciliation.generation_digest
AND item.task_id =
concat('pkg:', workflow.package_name, ':', source.task_id)
AND item.disposition IN ('created', 'retained', 'updated')
JOIN "ql3"."task_execution_revisions" AS execution
ON execution.project_id = workflow.project_id
AND execution.task_id = item.task_id
AND execution.source_revision = item.revision
AND execution.executor_type = 'remote_worker'
AND execution.source_content_digest = item.content_digest
WHERE workflow.run_id = p_run_id
FOR KEY SHARE OF workflow, source, reconciliation, item, execution;
END
$ql3$
`.trim(),
`
REVOKE ALL
ON "ql3"."plugin_package_workflow_task_attempt_admissions"
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_package_executor, ql3_worker_ingress
`.trim(),
`
GRANT SELECT, INSERT
ON "ql3"."plugin_package_workflow_task_attempt_admissions"
TO ql3_runtime
`.trim(),
`
REVOKE ALL ON FUNCTION
"ql3"."plugin_package_workflow_task_attempt_snapshot"(varchar, varchar)
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
ql3_package_executor, ql3_worker_ingress
`.trim(),
`
GRANT EXECUTE ON FUNCTION
"ql3"."plugin_package_workflow_task_attempt_snapshot"(varchar, varchar)
TO ql3_runtime
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 45,
migration_id =
'pg-0046-plugin-package-workflow-task-attempt-admissions',
capabilities = '${CAPABILITIES_V45}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 44
AND migration_id = 'pg-0045-plugin-package-workflow-admissions'
AND capabilities = '${CAPABILITIES_V44}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 44'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,175 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V45 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_task_attempt_admission":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V46 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_task_attempt_admission":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_management_plan":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0047WorkerCredentialManagementPlansMigration =
definePostgresSqlMigration({
id: 'pg-0047-worker-credential-management-plans',
statements: [
`
DO $ql3$
DECLARE
invalid_roles text[];
BEGIN
SELECT array_agg(expected.role_name ORDER BY expected.role_name)
INTO invalid_roles
FROM (
VALUES
('ql3_worker_credential_manager'),
('ql3_worker_credential_executor')
) AS expected(role_name)
LEFT JOIN pg_catalog.pg_roles roles
ON roles.rolname = expected.role_name
WHERE roles.rolname IS NULL
OR roles.rolcanlogin IS NOT TRUE
OR roles.rolsuper IS NOT FALSE
OR roles.rolcreatedb IS NOT FALSE
OR roles.rolcreaterole IS NOT FALSE
OR roles.rolreplication IS NOT FALSE
OR roles.rolbypassrls IS NOT FALSE;
IF invalid_roles IS NOT NULL THEN
RAISE EXCEPTION
'required QingLong Worker credential roles are missing or privileged: %',
invalid_roles
USING ERRCODE = 'insufficient_privilege';
END IF;
END
$ql3$
`.trim(),
`
DO $ql3$
BEGIN
EXECUTE format(
'GRANT CONNECT ON DATABASE %I TO ql3_worker_credential_manager, ql3_worker_credential_executor',
current_database()
);
END
$ql3$
`.trim(),
`GRANT USAGE ON SCHEMA "ql3" TO ql3_worker_credential_manager, ql3_worker_credential_executor`,
`GRANT SELECT ON "ql3"."schema_migrations", "ql3"."schema_capabilities" TO ql3_worker_credential_manager, ql3_worker_credential_executor`,
`
CREATE TABLE "ql3"."worker_credential_management_plans" (
action_ref varchar(255) PRIMARY KEY,
authority_project_id varchar(128) NOT NULL,
action varchar(16) NOT NULL,
delivery_id varchar(36) NOT NULL,
worker_id varchar(128) NOT NULL,
credential_id varchar(64) NOT NULL,
previous_credential_id varchar(64),
credential_not_before_at_ms bigint NOT NULL,
credential_expires_at_ms bigint NOT NULL,
deployment_target_digest char(64) NOT NULL,
deployment_generation varchar(128) NOT NULL,
requested_by_type varchar(16) NOT NULL,
requested_by_id varchar(255) NOT NULL,
planned_at_ms bigint NOT NULL,
expires_at_ms bigint NOT NULL,
plan_digest char(64) NOT NULL,
preview_digest char(64) NOT NULL,
plan_json jsonb NOT NULL,
CONSTRAINT ql3_worker_credential_management_plan_project_fk
FOREIGN KEY (authority_project_id)
REFERENCES "ql3"."projects" (id) ON DELETE RESTRICT,
CONSTRAINT ql3_worker_credential_management_plan_identity_check CHECK (
action_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$' AND
action IN ('issue', 'rotate') AND
delivery_id ~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' AND
worker_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
credential_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$' AND
(previous_credential_id IS NULL OR
previous_credential_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$') AND
deployment_generation ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
requested_by_type = 'user' AND
char_length(requested_by_id) BETWEEN 1 AND 255 AND
requested_by_id !~ '[[:cntrl:]]'
),
CONSTRAINT ql3_worker_credential_management_plan_action_check CHECK (
(action = 'issue' AND previous_credential_id IS NULL) OR
(action = 'rotate' AND previous_credential_id IS NOT NULL AND
previous_credential_id <> credential_id)
),
CONSTRAINT ql3_worker_credential_management_plan_digest_check CHECK (
deployment_target_digest ~ '^[0-9a-f]{64}$' AND
plan_digest ~ '^[0-9a-f]{64}$' AND
preview_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_worker_credential_management_plan_time_check CHECK (
planned_at_ms >= 0 AND
expires_at_ms > planned_at_ms AND
expires_at_ms - planned_at_ms <= 900000 AND
credential_not_before_at_ms >= planned_at_ms AND
credential_expires_at_ms > credential_not_before_at_ms AND
credential_expires_at_ms - credential_not_before_at_ms <= 63072000000
),
CONSTRAINT ql3_worker_credential_management_plan_json_check CHECK (
jsonb_typeof(plan_json) = 'object' AND
octet_length(plan_json::text) BETWEEN 2 AND 16384 AND
plan_json @> jsonb_build_object(
'schema', 'qinglong/worker-credential-management-plan@v1',
'actionRef', action_ref,
'authorityProjectId', authority_project_id,
'action', action,
'planDigest', plan_digest,
'previewDigest', preview_digest,
'requestedBy', jsonb_build_object(
'type', requested_by_type,
'id', requested_by_id
),
'plannedAtMs', planned_at_ms,
'expiresAtMs', expires_at_ms,
'target', jsonb_build_object(
'deliveryId', delivery_id,
'workerId', worker_id,
'credentialId', credential_id,
'previousCredentialId', previous_credential_id,
'credentialNotBeforeAtMs', credential_not_before_at_ms,
'credentialExpiresAtMs', credential_expires_at_ms,
'deploymentTargetDigest', deployment_target_digest,
'deploymentGeneration', deployment_generation
)
)
)
)
`.trim(),
`CREATE UNIQUE INDEX ql3_worker_credential_management_plan_digest_key ON "ql3"."worker_credential_management_plans" (plan_digest)`,
`CREATE UNIQUE INDEX ql3_worker_credential_management_plan_delivery_key ON "ql3"."worker_credential_management_plans" (delivery_id)`,
`CREATE INDEX ql3_worker_credential_management_plan_expiry_idx ON "ql3"."worker_credential_management_plans" (expires_at_ms, action_ref)`,
`REVOKE ALL ON "ql3"."worker_credential_management_plans" FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager, ql3_package_executor, ql3_worker_ingress, ql3_worker_credential_manager, ql3_worker_credential_executor`,
`GRANT SELECT, INSERT ON "ql3"."worker_credential_management_plans" TO ql3_worker_credential_manager`,
`GRANT SELECT ON "ql3"."worker_credential_management_plans" TO ql3_worker_credential_executor`,
`GRANT SELECT ON "ql3"."projects", "ql3"."project_role_bindings" TO ql3_worker_credential_manager, ql3_worker_credential_executor`,
`GRANT SELECT, INSERT ON "ql3"."security_audit_events" TO ql3_worker_credential_manager, ql3_worker_credential_executor`,
`GRANT SELECT, INSERT, UPDATE ON "ql3"."approval_requests" TO ql3_worker_credential_manager`,
`GRANT SELECT, UPDATE ON "ql3"."approval_requests" TO ql3_worker_credential_executor`,
`GRANT SELECT, INSERT ON "ql3"."approved_action_dispatches" TO ql3_worker_credential_executor`,
`GRANT SELECT, INSERT ON "ql3"."worker_credentials", "ql3"."worker_credential_mutations", "ql3"."worker_credential_deliveries", "ql3"."worker_credential_stage_discards" TO ql3_worker_credential_executor`,
`GRANT EXECUTE ON FUNCTION "ql3"."lock_approval_policy_fence"(varchar, varchar, varchar, integer, integer) TO ql3_worker_credential_manager, ql3_worker_credential_executor`,
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 46,
migration_id = 'pg-0047-worker-credential-management-plans',
capabilities = '${CAPABILITIES_V46}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 45
AND migration_id =
'pg-0046-plugin-package-workflow-task-attempt-admissions'
AND capabilities = '${CAPABILITIES_V45}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 45'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,36 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V46 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_task_attempt_admission":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_management_plan":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V47 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_task_attempt_admission":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_management_plan":1,"worker_credential_preapproved_activation":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0048WorkerCredentialPreapprovedActivationMigration =
definePostgresSqlMigration({
id: 'pg-0048-worker-credential-preapproved-activation',
statements: [
`ALTER TABLE "ql3"."worker_credentials" DROP CONSTRAINT ql3_worker_credentials_lifetime_check`,
`ALTER TABLE "ql3"."worker_credentials" ADD CONSTRAINT ql3_worker_credentials_lifetime_check CHECK (created_at_ms >= 0 AND not_before_at_ms >= 0 AND expires_at_ms > GREATEST(created_at_ms, not_before_at_ms))`,
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 47,
migration_id = 'pg-0048-worker-credential-preapproved-activation',
capabilities = '${CAPABILITIES_V47}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 46
AND migration_id = 'pg-0047-worker-credential-management-plans'
AND capabilities = '${CAPABILITIES_V46}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 46'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,36 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V47 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_task_attempt_admission":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_management_plan":1,"worker_credential_preapproved_activation":1,"worker_credential_stage_discard":1,"worker_session":1}';
const CAPABILITIES_V48 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_task_attempt_admission":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_execution_receipt":1,"worker_credential_management_plan":1,"worker_credential_preapproved_activation":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const pg0049WorkerCredentialExecutionReceiptsMigration =
definePostgresSqlMigration({
id: 'pg-0049-worker-credential-execution-receipts',
statements: [
`REVOKE ALL ON "ql3"."approved_action_executions" FROM ql3_worker_credential_manager, ql3_worker_credential_executor`,
`GRANT SELECT, INSERT, UPDATE ON "ql3"."approved_action_executions" TO ql3_worker_credential_executor`,
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 48,
migration_id = 'pg-0049-worker-credential-execution-receipts',
capabilities = '${CAPABILITIES_V48}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 47
AND migration_id = 'pg-0048-worker-credential-preapproved-activation'
AND capabilities = '${CAPABILITIES_V47}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 47'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,69 @@
import { definePostgresSqlMigration } from './sqlMigration';
const CAPABILITIES_V48 =
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_task_attempt_admission":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_execution_receipt":1,"worker_credential_management_plan":1,"worker_credential_preapproved_activation":1,"worker_credential_stage_discard":1,"worker_session":1}';
export const CAPABILITIES_V49 = CAPABILITIES_V48.replace(
'"worker_credential_management_plan":1,',
'"worker_credential_management_identity_keyset_ledger":1,"worker_credential_management_plan":1,"worker_credential_management_quota":1,',
);
export const pg0050WorkerCredentialManagementBoundaryMigration =
definePostgresSqlMigration({
id: 'pg-0050-worker-credential-management-boundary',
statements: [
`
CREATE TABLE "ql3"."worker_credential_management_quota_buckets" (
project_id varchar(128) NOT NULL,
subject_type varchar(32) NOT NULL,
subject_id varchar(255) NOT NULL,
operation varchar(64) NOT NULL,
window_started_at_ms bigint NOT NULL,
consumed_count integer NOT NULL,
receipt_ids jsonb NOT NULL,
updated_at_ms bigint NOT NULL,
CONSTRAINT worker_credential_management_quota_buckets_pkey
PRIMARY KEY (project_id, subject_type, subject_id, operation),
CONSTRAINT ql3_worker_credential_management_quota_project_fk
FOREIGN KEY (project_id) REFERENCES "ql3"."projects"(id) ON DELETE RESTRICT,
CONSTRAINT ql3_worker_credential_management_quota_identity_check
CHECK (subject_type = 'user' AND char_length(subject_id) BETWEEN 1 AND 255
AND subject_id !~ '[[:cntrl:]]'),
CONSTRAINT ql3_worker_credential_management_quota_operation_check
CHECK (operation IN ('worker-credential.plan', 'worker-credential.propose',
'worker-credential.decide', 'worker-credential.inspect')),
CONSTRAINT ql3_worker_credential_management_quota_window_check
CHECK (window_started_at_ms >= 0 AND consumed_count BETWEEN 1 AND 1000
AND updated_at_ms >= window_started_at_ms),
CONSTRAINT ql3_worker_credential_management_quota_receipts_check
CHECK (jsonb_typeof(receipt_ids) = 'array'
AND jsonb_array_length(receipt_ids) = consumed_count
AND octet_length(receipt_ids::text) BETWEEN 3 AND 262144)
)
`.trim(),
`REVOKE ALL ON "ql3"."worker_credential_management_quota_buckets" FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager, ql3_package_executor, ql3_worker_ingress, ql3_worker_credential_executor`,
`GRANT SELECT, INSERT, UPDATE ON "ql3"."worker_credential_management_quota_buckets" TO ql3_worker_credential_manager`,
`ALTER TABLE "ql3"."plugin_package_identity_keyset_ledger" DROP CONSTRAINT ql3_plugin_package_identity_keyset_authority_check`,
`ALTER TABLE "ql3"."plugin_package_identity_keyset_ledger" ADD CONSTRAINT ql3_plugin_package_identity_keyset_authority_check CHECK (authority IN ('plugin-package-management', 'worker-credential-management'))`,
`REVOKE ALL ON "ql3"."plugin_package_identity_keyset_ledger" FROM ql3_worker_credential_manager, ql3_worker_credential_executor`,
`GRANT SELECT, INSERT, UPDATE ON "ql3"."plugin_package_identity_keyset_ledger" TO ql3_worker_credential_manager`,
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 49,
migration_id = 'pg-0050-worker-credential-management-boundary',
capabilities = '${CAPABILITIES_V49}'::jsonb,
updated_at_ms = floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 48
AND migration_id = 'pg-0049-worker-credential-execution-receipts'
AND capabilities = '${CAPABILITIES_V48}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 48'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,77 @@
import { CAPABILITIES_V49 } from './pg-0050-worker-credential-management-boundary';
import { definePostgresSqlMigration } from './sqlMigration';
export const CAPABILITIES_V50 = CAPABILITIES_V49.replace(
'"approved_action":1,',
'"approved_action":1,"automation_management_boundary":1,',
);
export const pg0051AutomationManagementBoundaryMigration =
definePostgresSqlMigration({
id: 'pg-0051-automation-management-boundary',
statements: [
`
DO $ql3$
DECLARE
role_invalid boolean;
BEGIN
SELECT roles.rolname IS NULL
OR roles.rolcanlogin IS NOT TRUE
OR roles.rolsuper IS NOT FALSE
OR roles.rolcreatedb IS NOT FALSE
OR roles.rolcreaterole IS NOT FALSE
OR roles.rolreplication IS NOT FALSE
OR roles.rolbypassrls IS NOT FALSE
INTO role_invalid
FROM (SELECT 1) AS expected
LEFT JOIN pg_catalog.pg_roles AS roles
ON roles.rolname = 'ql3_automation_manager';
IF role_invalid IS NOT FALSE THEN
RAISE EXCEPTION
'required QingLong automation manager role is missing or privileged'
USING ERRCODE = 'insufficient_privilege';
END IF;
END
$ql3$
`.trim(),
`
DO $ql3$
BEGIN
EXECUTE format(
'GRANT CONNECT ON DATABASE %I TO ql3_automation_manager',
current_database()
);
END
$ql3$
`.trim(),
`GRANT USAGE ON SCHEMA "ql3" TO ql3_automation_manager`,
`REVOKE ALL ON ALL TABLES IN SCHEMA "ql3" FROM ql3_automation_manager`,
`REVOKE ALL ON ALL FUNCTIONS IN SCHEMA "ql3" FROM ql3_automation_manager`,
`GRANT SELECT ON "ql3"."schema_migrations", "ql3"."schema_capabilities", "ql3"."projects", "ql3"."project_role_bindings", "ql3"."plugin_package_task_ownerships" TO ql3_automation_manager`,
`GRANT SELECT, INSERT ON "ql3"."security_audit_events" TO ql3_automation_manager`,
`GRANT SELECT, INSERT, UPDATE ON "ql3"."task_definitions", "ql3"."triggers", "ql3"."trigger_schedules" TO ql3_automation_manager`,
`GRANT SELECT, INSERT ON "ql3"."task_definition_revisions", "ql3"."task_execution_revisions", "ql3"."trigger_revisions" TO ql3_automation_manager`,
`REVOKE SELECT, INSERT, UPDATE ON "ql3"."task_definitions", "ql3"."triggers", "ql3"."trigger_schedules" FROM ql3_admin`,
`REVOKE SELECT, INSERT ON "ql3"."task_definition_revisions", "ql3"."task_execution_revisions", "ql3"."trigger_revisions" FROM ql3_admin`,
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 50,
migration_id = 'pg-0051-automation-management-boundary',
capabilities = '${CAPABILITIES_V50}'::jsonb,
updated_at_ms = floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 49
AND migration_id = 'pg-0050-worker-credential-management-boundary'
AND capabilities = '${CAPABILITIES_V49}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 49'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,37 @@
import { CAPABILITIES_V50 } from './pg-0051-automation-management-boundary';
import { definePostgresSqlMigration } from './sqlMigration';
export const CAPABILITIES_V51 = CAPABILITIES_V50.replace(
'"automation_management_boundary":1,',
'"automation_management_boundary":1,"automation_management_identity_keyset_ledger":1,',
);
export const pg0052AutomationManagementIdentityKeysetLedgerMigration =
definePostgresSqlMigration({
id: 'pg-0052-automation-management-identity-keyset-ledger',
statements: [
`ALTER TABLE "ql3"."plugin_package_identity_keyset_ledger" DROP CONSTRAINT ql3_plugin_package_identity_keyset_authority_check`,
`ALTER TABLE "ql3"."plugin_package_identity_keyset_ledger" ADD CONSTRAINT ql3_plugin_package_identity_keyset_authority_check CHECK (authority IN ('plugin-package-management', 'worker-credential-management', 'automation-management'))`,
`REVOKE ALL ON "ql3"."plugin_package_identity_keyset_ledger" FROM ql3_automation_manager`,
`GRANT SELECT, INSERT, UPDATE ON "ql3"."plugin_package_identity_keyset_ledger" TO ql3_automation_manager`,
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 51,
migration_id = 'pg-0052-automation-management-identity-keyset-ledger',
capabilities = '${CAPABILITIES_V51}'::jsonb,
updated_at_ms = floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 50
AND migration_id = 'pg-0051-automation-management-boundary'
AND capabilities = '${CAPABILITIES_V50}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 50'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,34 @@
import { CAPABILITIES_V51 } from './pg-0052-automation-management-identity-keyset-ledger';
import { definePostgresSqlMigration } from './sqlMigration';
export const CAPABILITIES_V52 = CAPABILITIES_V51.replace(
'"plugin_package_workflow_admission":1,',
'"plugin_package_workflow_admission":1,"plugin_package_workflow_run_list":1,',
);
export const pg0053PluginPackageWorkflowRunListIndexMigration =
definePostgresSqlMigration({
id: 'pg-0053-plugin-package-workflow-run-list-index',
statements: [
`CREATE INDEX ql3_plugin_package_workflow_admission_workflow_history_idx ON "ql3"."plugin_package_workflow_admissions" (project_id, package_name, workflow_id, admitted_at_ms, run_id)`,
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 52,
migration_id = 'pg-0053-plugin-package-workflow-run-list-index',
capabilities = '${CAPABILITIES_V52}'::jsonb,
updated_at_ms = floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 51
AND migration_id = 'pg-0052-automation-management-identity-keyset-ledger'
AND capabilities = '${CAPABILITIES_V51}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 51'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,78 @@
import { CAPABILITIES_V52 } from './pg-0053-plugin-package-workflow-run-list-index';
import { definePostgresSqlMigration } from './sqlMigration';
export const CAPABILITIES_V53 = CAPABILITIES_V52.replace(
'"approved_action":1,',
'"approval_management_boundary":1,"approved_action":1,',
);
export const pg0054ApprovalManagementBoundaryMigration =
definePostgresSqlMigration({
id: 'pg-0054-approval-management-boundary',
statements: [
`
DO $ql3$
DECLARE
role_invalid boolean;
BEGIN
SELECT roles.rolname IS NULL
OR roles.rolcanlogin IS NOT TRUE
OR roles.rolsuper IS NOT FALSE
OR roles.rolcreatedb IS NOT FALSE
OR roles.rolcreaterole IS NOT FALSE
OR roles.rolreplication IS NOT FALSE
OR roles.rolbypassrls IS NOT FALSE
INTO role_invalid
FROM (SELECT 1) AS expected
LEFT JOIN pg_catalog.pg_roles AS roles
ON roles.rolname = 'ql3_approval_manager';
IF role_invalid IS NOT FALSE THEN
RAISE EXCEPTION
'required QingLong approval manager role is missing or privileged'
USING ERRCODE = 'insufficient_privilege';
END IF;
END
$ql3$
`.trim(),
`
DO $ql3$
BEGIN
EXECUTE format(
'GRANT CONNECT ON DATABASE %I TO ql3_approval_manager',
current_database()
);
END
$ql3$
`.trim(),
`GRANT USAGE ON SCHEMA "ql3" TO ql3_approval_manager`,
`REVOKE ALL ON ALL TABLES IN SCHEMA "ql3" FROM ql3_approval_manager`,
`REVOKE ALL ON ALL FUNCTIONS IN SCHEMA "ql3" FROM ql3_approval_manager`,
`GRANT SELECT ON "ql3"."schema_migrations", "ql3"."schema_capabilities", "ql3"."projects", "ql3"."project_role_bindings", "ql3"."tool_invocation_preview_artifacts" TO ql3_approval_manager`,
`GRANT SELECT, INSERT ON "ql3"."security_audit_events" TO ql3_approval_manager`,
`GRANT SELECT, UPDATE ON "ql3"."approval_requests" TO ql3_approval_manager`,
`GRANT SELECT, INSERT, UPDATE ON "ql3"."plugin_package_identity_keyset_ledger" TO ql3_approval_manager`,
`GRANT EXECUTE ON FUNCTION "ql3"."lock_approval_policy_fence"(varchar, varchar, varchar, integer, integer) TO ql3_approval_manager`,
`ALTER TABLE "ql3"."plugin_package_identity_keyset_ledger" DROP CONSTRAINT ql3_plugin_package_identity_keyset_authority_check`,
`ALTER TABLE "ql3"."plugin_package_identity_keyset_ledger" ADD CONSTRAINT ql3_plugin_package_identity_keyset_authority_check CHECK (authority IN ('plugin-package-management', 'worker-credential-management', 'automation-management', 'approval-management'))`,
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 53,
migration_id = 'pg-0054-approval-management-boundary',
capabilities = '${CAPABILITIES_V53}'::jsonb,
updated_at_ms = floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 52
AND migration_id = 'pg-0053-plugin-package-workflow-run-list-index'
AND capabilities = '${CAPABILITIES_V52}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 52'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});
@@ -0,0 +1,262 @@
import {
MigrationStreamHistoryCorruptionError,
type MigrationStreamRecord,
type MigrationStreamStore,
type MigrationStreamTransaction,
} from '@qinglong/runtime-core';
import type {
PostgresClient as PostgresMigrationClient,
PostgresPool as PostgresMigrationPool,
PostgresQueryable as PostgresMigrationQueryable,
PostgresQueryResult as PostgresMigrationQueryResult,
} from '@qinglong/runtime-core';
export type {
PostgresClient as PostgresMigrationClient,
PostgresPool as PostgresMigrationPool,
PostgresQueryable as PostgresMigrationQueryable,
PostgresQueryResult as PostgresMigrationQueryResult,
} from '@qinglong/runtime-core';
export const POSTGRESQL_MAIN_MIGRATION_STREAM_ID = 'postgresql-main';
export const POSTGRESQL_MIGRATION_SCHEMA = 'ql3';
export const POSTGRESQL_MIGRATION_HISTORY_TABLE = 'schema_migrations';
const POSTGRESQL_MIGRATION_LOCK_KEY = [0x514c, 0x0300] as const;
const POSTGRESQL_MIGRATION_STATEMENT_TIMEOUT_MS = 15_000;
const POSTGRESQL_MIGRATION_LOCK_TIMEOUT_MS = 5_000;
const POSTGRESQL_MIGRATION_IDLE_TRANSACTION_TIMEOUT_MS = 15_000;
export type PostgresMigrationContext = PostgresMigrationQueryable;
export class PostgresMigrationLeaderUnavailableError extends Error {
constructor() {
super('PostgreSQL migration leader lock is unavailable');
this.name = 'PostgresMigrationLeaderUnavailableError';
}
}
interface HistoryRow extends Record<string, unknown> {
streamId: unknown;
dialect: unknown;
migrationId: unknown;
checksum: unknown;
appliedAtMs: unknown;
}
interface AdvisoryLockRow extends Record<string, unknown> {
acquired: unknown;
}
const CREATE_HISTORY_SQL = `
CREATE TABLE IF NOT EXISTS "${POSTGRESQL_MIGRATION_SCHEMA}"."${POSTGRESQL_MIGRATION_HISTORY_TABLE}" (
migration_id varchar(128) PRIMARY KEY,
stream_id varchar(64) NOT NULL,
dialect varchar(16) NOT NULL
CONSTRAINT ql3_schema_migrations_dialect_check
CHECK (dialect = 'postgresql'),
checksum char(64) NOT NULL
CONSTRAINT ql3_schema_migrations_checksum_check
CHECK (checksum ~ '^[0-9a-f]{64}$'),
applied_at_ms bigint NOT NULL
CONSTRAINT ql3_schema_migrations_applied_at_check
CHECK (applied_at_ms >= 0)
)`.trim();
const SELECT_HISTORY_SQL = `
SELECT
stream_id AS "streamId",
dialect,
migration_id AS "migrationId",
checksum,
applied_at_ms AS "appliedAtMs"
FROM "${POSTGRESQL_MIGRATION_SCHEMA}"."${POSTGRESQL_MIGRATION_HISTORY_TABLE}"
WHERE migration_id = $1
`.trim();
const SELECT_ALL_HISTORY_SQL = `
SELECT
stream_id AS "streamId",
dialect,
migration_id AS "migrationId",
checksum,
applied_at_ms AS "appliedAtMs"
FROM "${POSTGRESQL_MIGRATION_SCHEMA}"."${POSTGRESQL_MIGRATION_HISTORY_TABLE}"
ORDER BY applied_at_ms, migration_id
`.trim();
const INSERT_HISTORY_SQL = `
INSERT INTO "${POSTGRESQL_MIGRATION_SCHEMA}"."${POSTGRESQL_MIGRATION_HISTORY_TABLE}" (
migration_id,
stream_id,
dialect,
checksum,
applied_at_ms
)
VALUES ($1, $2, $3, $4, $5)
`.trim();
function parseAppliedAtMs(value: unknown, migrationId: string): number {
if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) {
return value;
}
if (typeof value === 'string' && /^(0|[1-9]\d*)$/.test(value)) {
const parsed = Number(value);
if (Number.isSafeInteger(parsed)) return parsed;
}
throw new MigrationStreamHistoryCorruptionError(migrationId);
}
async function findHistoryRecord(
queryable: PostgresMigrationQueryable,
migrationId: string,
): Promise<MigrationStreamRecord | null> {
const result = await queryable.query<HistoryRow>(SELECT_HISTORY_SQL, [
migrationId,
]);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) {
throw new MigrationStreamHistoryCorruptionError(migrationId);
}
const row = result.rows[0];
if (!row) {
throw new MigrationStreamHistoryCorruptionError(migrationId);
}
return {
streamId: row.streamId as string,
dialect: row.dialect as 'postgresql',
migrationId: row.migrationId as string,
checksum: row.checksum as string,
appliedAtMs: parseAppliedAtMs(row.appliedAtMs, migrationId),
};
}
export async function readPostgresMigrationHistory(
queryable: PostgresMigrationQueryable,
): Promise<readonly MigrationStreamRecord[]> {
const result = await queryable.query<HistoryRow>(SELECT_ALL_HISTORY_SQL);
return result.rows.map((row) => ({
streamId: row.streamId as string,
dialect: row.dialect as 'postgresql',
migrationId: row.migrationId as string,
checksum: row.checksum as string,
appliedAtMs: parseAppliedAtMs(
row.appliedAtMs,
typeof row.migrationId === 'string' ? row.migrationId : 'unknown',
),
}));
}
async function configureTransaction(
client: PostgresMigrationClient,
): Promise<void> {
await client.query(`SELECT set_config('statement_timeout', $1, true)`, [
`${POSTGRESQL_MIGRATION_STATEMENT_TIMEOUT_MS}ms`,
]);
await client.query(`SELECT set_config('lock_timeout', $1, true)`, [
`${POSTGRESQL_MIGRATION_LOCK_TIMEOUT_MS}ms`,
]);
await client.query(
`SELECT set_config('idle_in_transaction_session_timeout', $1, true)`,
[`${POSTGRESQL_MIGRATION_IDLE_TRANSACTION_TIMEOUT_MS}ms`],
);
}
async function acquireLeaderLock(
client: PostgresMigrationClient,
): Promise<void> {
const result = await client.query<AdvisoryLockRow>(
'SELECT pg_try_advisory_xact_lock($1, $2) AS acquired',
POSTGRESQL_MIGRATION_LOCK_KEY,
);
if (result.rows.length !== 1 || result.rows[0]?.acquired !== true) {
throw new PostgresMigrationLeaderUnavailableError();
}
}
/**
* PostgreSQL migration history adapter without a runtime dependency on `pg`.
* The cluster-only package owns the concrete Pool binding; edge and standalone
* builds can compile this contract without installing or importing the driver.
*/
export class PostgresMigrationStreamStore
implements MigrationStreamStore<PostgresMigrationContext>
{
constructor(private readonly pool: PostgresMigrationPool) {}
async ensureHistory(): Promise<void> {
await this.withLeaderTransaction(async (client) => {
await client.query(
`CREATE SCHEMA IF NOT EXISTS "${POSTGRESQL_MIGRATION_SCHEMA}"`,
);
await client.query(CREATE_HISTORY_SQL);
});
}
async findById(migrationId: string): Promise<MigrationStreamRecord | null> {
return findHistoryRecord(this.pool, migrationId);
}
async listAll(): Promise<readonly MigrationStreamRecord[]> {
return readPostgresMigrationHistory(this.pool);
}
async transaction<T>(
work: (
transaction: MigrationStreamTransaction<PostgresMigrationContext>,
) => Promise<T>,
): Promise<T> {
return this.withLeaderTransaction(async (client) =>
work({
context: client,
findById: (migrationId) => findHistoryRecord(client, migrationId),
insert: async (record) => {
if (
record.streamId !== POSTGRESQL_MAIN_MIGRATION_STREAM_ID ||
record.dialect !== 'postgresql'
) {
throw new TypeError(
'PostgreSQL migration history record has the wrong stream or dialect',
);
}
await client.query(INSERT_HISTORY_SQL, [
record.migrationId,
record.streamId,
record.dialect,
record.checksum,
record.appliedAtMs,
]);
},
}),
);
}
private async withLeaderTransaction<T>(
work: (client: PostgresMigrationClient) => Promise<T>,
): Promise<T> {
const client = await this.pool.connect();
let began = false;
try {
await client.query('BEGIN');
began = true;
await configureTransaction(client);
await acquireLeaderLock(client);
const result = await work(client);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (began) {
try {
await client.query('ROLLBACK');
} catch {
// Preserve the migration error. A broken connection is discarded by
// the concrete driver when release() runs.
}
}
throw error;
} finally {
client.release();
}
}
}
@@ -0,0 +1,34 @@
import { createHash } from 'crypto';
import type { MigrationStreamStep } from '@qinglong/runtime-core';
import type { PostgresMigrationContext } from './postgresMigrationStreamStore';
export interface PostgresSqlMigrationDefinition {
readonly id: string;
readonly statements: readonly string[];
}
export function checksumPostgresStatements(
statements: readonly string[],
): string {
return createHash('sha256')
.update(
JSON.stringify({
format: 1,
statements,
}),
)
.digest('hex');
}
export function definePostgresSqlMigration(
definition: PostgresSqlMigrationDefinition,
): MigrationStreamStep<PostgresMigrationContext> {
const statements = Object.freeze([...definition.statements]);
return Object.freeze({
id: definition.id,
checksum: checksumPostgresStatements(statements),
async up(context: PostgresMigrationContext) {
for (const statement of statements) await context.query(statement);
},
});
}
@@ -0,0 +1,222 @@
// PostgreSQL post-install resource materialization and recovery authority.
import type { PostgresPool } from '@qinglong/runtime-core';
import {
InvalidPluginPackageResourceMaterializationError,
MAX_PLUGIN_PACKAGE_MATERIALIZED_REVISION_JSON_BYTES,
PluginPackageResourceMaterializationConflictError,
PluginPackageResourceMaterializationUnavailableError,
normalizePluginPackageMaterializedRevision,
type PluginPackageMaterializedRevision,
type PluginPackageMaterializedRevisionRepository,
} from '@qinglong/runtime-core/plugin-package-resource-materialization';
import {
TaskSpecSemanticRegistry,
createBuiltInTaskSpecSemanticRegistry,
} from '@qinglong/runtime-core/task-spec-semantic';
import {
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
const DIGEST = /^[0-9a-f]{64}$/;
function invalid(message: string): never {
throw new InvalidPluginPackageResourceMaterializationError(message);
}
function unavailable(): PluginPackageResourceMaterializationUnavailableError {
return new PluginPackageResourceMaterializationUnavailableError();
}
function generationDigest(value: unknown): string {
if (typeof value !== 'string' || !DIGEST.test(value)) {
return invalid('generation digest is invalid');
}
return value;
}
function serialize(
revision: Readonly<PluginPackageMaterializedRevision>,
): string {
const value = JSON.stringify(revision);
if (
Buffer.byteLength(value, 'utf8') >
MAX_PLUGIN_PACKAGE_MATERIALIZED_REVISION_JSON_BYTES
) {
return invalid('materialized revision exceeds the durable JSON budget');
}
return value;
}
function mapStorageError(error: unknown): Error {
if (
error instanceof InvalidPluginPackageResourceMaterializationError ||
error instanceof PluginPackageResourceMaterializationConflictError ||
error instanceof PluginPackageResourceMaterializationUnavailableError
) {
return error;
}
const state = postgresSqlState(error);
if (state === '23503' || state === '23505' || state === '23514') {
return new PluginPackageResourceMaterializationConflictError(
'durable revision identity is already bound',
);
}
return new PluginPackageResourceMaterializationUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
export class PostgresPluginPackageMaterializedRevisionRepository
implements PluginPackageMaterializedRevisionRepository
{
private readonly taskSpecSemanticRegistry: TaskSpecSemanticRegistry;
constructor(
private readonly pool: Pick<PostgresPool, 'query'>,
taskSpecSemanticRegistry = createBuiltInTaskSpecSemanticRegistry(),
) {
if (
!pool ||
typeof pool.query !== 'function' ||
!(taskSpecSemanticRegistry instanceof TaskSpecSemanticRegistry)
) {
throw new TypeError(
'PostgreSQL materialized revision repository options are invalid',
);
}
this.taskSpecSemanticRegistry = taskSpecSemanticRegistry;
}
private parse(row: Row): Readonly<PluginPackageMaterializedRevision> {
try {
const revision = normalizePluginPackageMaterializedRevision(
postgresRequiredJsonObject(
row.revisionJson,
unavailable,
) as unknown as PluginPackageMaterializedRevision,
this.taskSpecSemanticRegistry,
);
if (
revision.generation.generationDigest !==
postgresRequiredString(row.generationDigest, unavailable) ||
revision.generation.projectId !==
postgresRequiredString(row.projectId, unavailable) ||
revision.generation.packageName !==
postgresRequiredString(row.packageName, unavailable) ||
revision.generation.generation !==
postgresRequiredInteger(row.generation, unavailable) ||
revision.generation.lockDigest !==
postgresRequiredString(row.lockDigest, unavailable) ||
revision.manifestDigest !==
postgresRequiredString(row.manifestDigest, unavailable) ||
revision.revisionDigest !==
postgresRequiredString(row.revisionDigest, unavailable)
) {
throw unavailable();
}
const createdAtMs = postgresRequiredInteger(row.createdAtMs, unavailable);
if (createdAtMs < 0) throw unavailable();
return revision;
} catch (error) {
if (error instanceof PluginPackageResourceMaterializationUnavailableError) {
throw error;
}
throw unavailable();
}
}
private async findStored(
digest: string,
): Promise<Readonly<PluginPackageMaterializedRevision> | null> {
const result = await this.pool.query<Row>(
`SELECT generation_digest AS "generationDigest",
project_id AS "projectId",
package_name AS "packageName",
generation,
lock_digest AS "lockDigest",
manifest_digest AS "manifestDigest",
revision_digest AS "revisionDigest",
revision_json AS "revisionJson",
created_at_ms AS "createdAtMs"
FROM "ql3"."plugin_package_materialized_revisions"
WHERE generation_digest = $1
LIMIT 2`,
[digest],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return this.parse(result.rows[0]!);
}
async find(
digest: string,
): Promise<Readonly<PluginPackageMaterializedRevision> | null> {
try {
return await this.findStored(generationDigest(digest));
} catch (error) {
throw mapStorageError(error);
}
}
async publish(
value: Readonly<PluginPackageMaterializedRevision>,
): Promise<
Readonly<{
status: 'created' | 'existing';
revision: Readonly<PluginPackageMaterializedRevision>;
}>
> {
const revision = normalizePluginPackageMaterializedRevision(
value,
this.taskSpecSemanticRegistry,
);
const revisionJson = serialize(revision);
try {
const inserted = await this.pool.query(
`INSERT INTO "ql3"."plugin_package_materialized_revisions" (
generation_digest, project_id, package_name, generation,
lock_digest, manifest_digest, revision_digest, revision_json,
created_at_ms
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb,
floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint)
ON CONFLICT (generation_digest) DO NOTHING
RETURNING generation_digest`,
[
revision.generation.generationDigest,
revision.generation.projectId,
revision.generation.packageName,
revision.generation.generation,
revision.generation.lockDigest,
revision.manifestDigest,
revision.revisionDigest,
revisionJson,
],
);
const stored = await this.findStored(
revision.generation.generationDigest,
);
if (!stored) throw unavailable();
if (
stored.revisionDigest !== revision.revisionDigest ||
JSON.stringify(stored) !== revisionJson
) {
throw new PluginPackageResourceMaterializationConflictError(
'generation digest is bound to another semantic revision',
);
}
return Object.freeze({
status: inserted.rows.length === 1 ? 'created' : 'existing',
revision: stored,
});
} catch (error) {
throw mapStorageError(error);
}
}
}
@@ -0,0 +1,341 @@
// PostgreSQL review proposal authority for Plugin Package installation.
import type {
PostgresClient,
PostgresPool,
} from '@qinglong/runtime-core';
import {
PluginPackageInstallProposalConflictError,
PluginPackageInstallProposalUnavailableError,
normalizePluginPackageInstallProposal,
type CreatePluginPackageInstallProposalCommand,
type CreatePluginPackageInstallProposalResult,
type PluginPackageInstallProposal,
type PluginPackageInstallProposalRepository,
} from '@qinglong/runtime-core/plugin-package-proposal';
import {
normalizeSecurityAuditRecord,
type SecurityAuditRecord,
} from '@qinglong/runtime-core/security-audit';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredBoolean,
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
type Queryable = Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>;
function unavailable(): PluginPackageInstallProposalUnavailableError {
return new PluginPackageInstallProposalUnavailableError();
}
function nullableString(value: unknown): string | null {
return value === null ? null : postgresRequiredString(value, unavailable);
}
function nullableInteger(value: unknown): number | null {
return value === null ? null : postgresRequiredInteger(value, unavailable);
}
function same(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function parseProposal(row: Row): Readonly<PluginPackageInstallProposal> {
try {
const proposal = normalizePluginPackageInstallProposal(
postgresRequiredJsonObject(
row.proposalJson,
unavailable,
) as unknown as PluginPackageInstallProposal,
);
if (
proposal.proposalDigest !==
postgresRequiredString(row.proposalDigest, unavailable)
) {
throw unavailable();
}
return proposal;
} catch (error) {
if (error instanceof PluginPackageInstallProposalUnavailableError) {
throw error;
}
throw unavailable();
}
}
function parseAudit(row: Row): Readonly<SecurityAuditRecord> {
try {
const subjectType = nullableString(row.subjectType);
const subjectId = nullableString(row.subjectId);
const projectVersion = nullableInteger(row.projectVersion);
if (!Array.isArray(row.reasons)) throw unavailable();
return normalizeSecurityAuditRecord({
eventId: postgresRequiredString(row.eventId, unavailable),
requestId: postgresRequiredString(row.requestId, unavailable),
operationId: postgresRequiredString(row.operationId, unavailable),
projectId: nullableString(row.projectId),
subject:
subjectType === null || subjectId === null
? null
: { type: subjectType, id: subjectId },
authenticationId: nullableString(row.authenticationId),
outcome: postgresRequiredString(row.outcome, unavailable),
reasons: row.reasons,
fence:
projectVersion === null
? null
: {
projectVersion,
bindingVersion: nullableInteger(row.bindingVersion),
},
occurredAtMs: postgresRequiredInteger(row.occurredAtMs, unavailable),
} as SecurityAuditRecord);
} catch (error) {
if (error instanceof PluginPackageInstallProposalUnavailableError) {
throw error;
}
throw unavailable();
}
}
function mappedError(error: unknown): Error {
if (
error instanceof PluginPackageInstallProposalConflictError ||
error instanceof PluginPackageInstallProposalUnavailableError
) {
return error;
}
const state = postgresSqlState(error);
if (state === '23503' || state === '23505' || state === '23514') {
return new PluginPackageInstallProposalConflictError();
}
return new PluginPackageInstallProposalUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
async function proposalByActionRef(
queryable: Queryable,
actionRef: string,
): Promise<Readonly<PluginPackageInstallProposal> | null> {
const result = await queryable.query<Row>(
`SELECT proposal_json AS "proposalJson",
proposal_digest AS "proposalDigest"
FROM "ql3"."plugin_package_install_proposals"
WHERE action_ref = $1
LIMIT 2`,
[actionRef],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return parseProposal(result.rows[0]!);
}
export function findPostgresPluginPackageInstallProposal(
queryable: Queryable,
actionRef: string,
): Promise<Readonly<PluginPackageInstallProposal> | null> {
return proposalByActionRef(queryable, actionRef);
}
async function auditById(
queryable: Queryable,
eventId: string,
): Promise<Readonly<SecurityAuditRecord> | null> {
const result = await queryable.query<Row>(
`SELECT event_id AS "eventId", request_id AS "requestId",
operation_id AS "operationId", project_id AS "projectId",
subject_type AS "subjectType", subject_id AS "subjectId",
authentication_id AS "authenticationId", outcome,
reasons, project_version AS "projectVersion",
binding_version AS "bindingVersion",
occurred_at_ms AS "occurredAtMs"
FROM "ql3"."security_audit_events"
WHERE event_id = $1
LIMIT 2`,
[eventId],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return parseAudit(result.rows[0]!);
}
export class PostgresPluginPackageInstallProposalRepository
implements PluginPackageInstallProposalRepository
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError('PostgreSQL Package proposal pool is invalid');
}
}
async #transaction<T>(
work: (client: PostgresClient) => Promise<T>,
): Promise<T> {
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
throw mappedError(error);
}
let began = false;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const result = await work(client);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
state &&
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
) {
continue;
}
throw mappedError(error);
} finally {
client.release();
}
}
throw unavailable();
}
async findProposalByActionRef(
actionRef: string,
): Promise<Readonly<PluginPackageInstallProposal> | null> {
try {
return await proposalByActionRef(this.pool, actionRef);
} catch (error) {
throw mappedError(error);
}
}
createProposal(
command: CreatePluginPackageInstallProposalCommand,
): Promise<Readonly<CreatePluginPackageInstallProposalResult>> {
const proposal = normalizePluginPackageInstallProposal(command.proposal);
const audit = normalizeSecurityAuditRecord(command.audit);
if (
audit.requestId !== proposal.actionRef ||
audit.operationId !== 'plugin_package.propose' ||
audit.projectId !== proposal.projectId ||
audit.subject?.type !== proposal.proposedBy.type ||
audit.subject.id !== proposal.proposedBy.id ||
audit.authenticationId === null ||
audit.outcome !== 'allowed' ||
!same(audit.reasons, ['package_proposal']) ||
audit.fence?.projectVersion !== proposal.proposalFence.projectVersion ||
audit.fence.bindingVersion !== proposal.proposalFence.bindingVersion ||
audit.occurredAtMs !== proposal.createdAtMs
) {
throw new PluginPackageInstallProposalConflictError();
}
return this.#transaction(async (client) => {
const existing = await proposalByActionRef(client, proposal.actionRef);
if (existing) {
const existingAudit = await auditById(client, audit.eventId);
if (
!same(existing, proposal) ||
!existingAudit ||
!same(existingAudit, audit)
) {
throw new PluginPackageInstallProposalConflictError();
}
return Object.freeze({
status: 'existing' as const,
proposal,
});
}
const fence = await client.query<Row>(
`SELECT "ql3"."lock_approval_policy_fence"(
$1::varchar, $2::varchar, $3::varchar, $4::integer, $5::integer
) AS "matches"`,
[
proposal.projectId,
proposal.proposedBy.type,
proposal.proposedBy.id,
proposal.proposalFence.projectVersion,
proposal.proposalFence.bindingVersion,
],
);
if (
fence.rows.length !== 1 ||
!postgresRequiredBoolean(fence.rows[0]!.matches, unavailable)
) {
throw new PluginPackageInstallProposalConflictError();
}
const inserted = await client.query(
`INSERT INTO "ql3"."plugin_package_install_proposals" (
action_ref, project_id, action_type, permission, action_digest,
preview_digest, proposed_by_type, proposed_by_id,
fence_project_version, fence_binding_version, created_at_ms,
proposal_json, proposal_digest
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12::jsonb, $13
)`,
[
proposal.actionRef,
proposal.projectId,
proposal.actionType,
proposal.permission,
proposal.actionDigest,
proposal.previewDigest,
proposal.proposedBy.type,
proposal.proposedBy.id,
proposal.proposalFence.projectVersion,
proposal.proposalFence.bindingVersion,
proposal.createdAtMs,
JSON.stringify(proposal),
proposal.proposalDigest,
],
);
if (inserted.rowCount !== 1) throw unavailable();
const auditInserted = await client.query(
`INSERT INTO "ql3"."security_audit_events" (
event_id, request_id, operation_id, project_id, subject_type,
subject_id, authentication_id, outcome, reasons,
project_version, binding_version, occurred_at_ms
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11, $12
)`,
[
audit.eventId,
audit.requestId,
audit.operationId,
audit.projectId,
audit.subject?.type ?? null,
audit.subject?.id ?? null,
audit.authenticationId,
audit.outcome,
JSON.stringify(audit.reasons),
audit.fence?.projectVersion ?? null,
audit.fence?.bindingVersion ?? null,
audit.occurredAtMs,
],
);
if (auditInserted.rowCount !== 1) throw unavailable();
return Object.freeze({ status: 'created' as const, proposal });
});
}
}
@@ -0,0 +1,159 @@
// PostgreSQL adapter owned by the Plugin Package lifecycle capability.
import type { PostgresPool } from '@qinglong/runtime-core';
import {
InvalidPluginPackageLifecyclePlanError,
PluginPackageLifecyclePlanConflictError,
PluginPackageLifecyclePlanUnavailableError,
normalizePluginPackageLifecyclePlan,
type CreatePluginPackageLifecyclePlanResult,
type PluginPackageLifecyclePlan,
type PluginPackageLifecyclePlanRepository,
} from '@qinglong/runtime-core/plugin-package-lifecycle-plan';
import {
postgresRequiredJsonObject,
postgresSqlState,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
const ACTION_REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$/;
function unavailable(
cause?: unknown,
): PluginPackageLifecyclePlanUnavailableError {
return new PluginPackageLifecyclePlanUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function mapStorageError(error: unknown): Error {
if (
error instanceof InvalidPluginPackageLifecyclePlanError ||
error instanceof PluginPackageLifecyclePlanConflictError ||
error instanceof PluginPackageLifecyclePlanUnavailableError
) {
return error;
}
const state = postgresSqlState(error);
if (state === '23503' || state === '23505' || state === '23514') {
return new PluginPackageLifecyclePlanConflictError(
'plan identity or target is already bound',
);
}
return unavailable(error);
}
function same(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function normalizeRow(row: Row): Readonly<PluginPackageLifecyclePlan> {
try {
return normalizePluginPackageLifecyclePlan(
postgresRequiredJsonObject(
row.planJson,
unavailable,
) as unknown as PluginPackageLifecyclePlan,
);
} catch (error) {
throw unavailable(error);
}
}
function validateActionRef(value: string): string {
if (typeof value !== 'string' || !ACTION_REF_PATTERN.test(value)) {
throw new InvalidPluginPackageLifecyclePlanError(
'actionRef is invalid',
);
}
return value;
}
export class PostgresPluginPackageLifecyclePlanReader {
constructor(protected readonly pool: PostgresPool) {
if (
!pool ||
typeof pool !== 'object' ||
typeof pool.query !== 'function'
) {
throw new TypeError(
'PostgreSQL Plugin Package lifecycle plan reader is invalid',
);
}
}
async findByActionRef(
actionRef: string,
): Promise<Readonly<PluginPackageLifecyclePlan> | null> {
validateActionRef(actionRef);
try {
const result = await this.pool.query<Row>(
`SELECT plan_json AS "planJson"
FROM "ql3"."plugin_package_lifecycle_plans"
WHERE action_ref = $1
LIMIT 2`,
[actionRef],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
const plan = normalizeRow(result.rows[0]!);
if (plan.actionRef !== actionRef) throw unavailable();
return plan;
} catch (error) {
throw mapStorageError(error);
}
}
}
export class PostgresPluginPackageLifecyclePlanRepository
extends PostgresPluginPackageLifecyclePlanReader
implements PluginPackageLifecyclePlanRepository
{
async create(
planValue: Readonly<PluginPackageLifecyclePlan>,
): Promise<Readonly<CreatePluginPackageLifecyclePlanResult>> {
const plan = normalizePluginPackageLifecyclePlan(planValue);
try {
const inserted = await this.pool.query<Row>(
`INSERT INTO "ql3"."plugin_package_lifecycle_plans" (
action_ref, plan_digest, action, project_id, package_name,
installation_id, lock_digest, impact_digest, requested_by_type,
requested_by_id, planned_at_ms, expires_at_ms, plan_json
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13::jsonb
)
ON CONFLICT DO NOTHING
RETURNING action_ref AS "actionRef"`,
[
plan.actionRef,
plan.planDigest,
plan.impact.action,
plan.impact.target.projectId,
plan.impact.target.packageName,
plan.impact.target.installationId,
plan.impact.target.lockDigest,
plan.impact.impactDigest,
plan.requestedBy.type,
plan.requestedBy.id,
plan.plannedAtMs,
plan.expiresAtMs,
JSON.stringify(plan),
],
);
const stored = await this.findByActionRef(plan.actionRef);
if (!stored || !same(stored, plan)) {
throw new PluginPackageLifecyclePlanConflictError(
'plan identity is already bound to another impact',
);
}
return Object.freeze({
status:
inserted.rows.length === 1 ? ('created' as const) : ('existing' as const),
plan: stored,
});
} catch (error) {
throw mapStorageError(error);
}
}
}
@@ -0,0 +1,787 @@
// PostgreSQL adapter owned by the Plugin Package lifecycle capability.
import type {
PostgresClient,
PostgresPool,
PostgresQueryable,
} from '@qinglong/runtime-core';
import {
InvalidPluginPackageQuarantineError,
MAX_PLUGIN_PACKAGE_QUARANTINE_RETAINED_SOURCES,
MAX_PLUGIN_PACKAGE_QUARANTINE_TASK_WITHDRAWALS,
PluginPackageQuarantineConflictError,
PluginPackageQuarantineUnavailableError,
assertPluginPackageWithdrawalMatchesEvent,
createPluginPackageWithdrawalReceipt,
normalizePluginPackageQuarantineEvent,
normalizePluginPackageWithdrawalReceipt,
pluginPackageQuarantineTaskMutationId,
type PluginPackageQuarantineEvent,
type PluginPackageQuarantineRepository,
type PluginPackageQuarantineTarget,
type PluginPackageQuarantineTaskWithdrawal,
type PluginPackageWithdrawalReceipt,
} from '@qinglong/runtime-core/plugin-package-quarantine';
import {
normalizePluginPackageInstallRecord,
type PluginPackageInstallRecord,
} from '@qinglong/runtime-core/plugin-package-install';
import {
createProjectToolDefinitionSnapshot,
normalizeProjectToolDefinitionSnapshot,
projectToolDefinitionActiveVectorDigest,
projectToolDefinitionSnapshotContribution,
type ProjectToolDefinitionSnapshot,
type ProjectToolDefinitionSnapshotContribution,
type ProjectToolDefinitionSnapshotSource,
} from '@qinglong/runtime-core/project-tool-definition-snapshot';
import {
createTaskDefinitionRecord,
normalizeTaskDefinitionRecord,
type TaskDefinitionRecord,
} from '@qinglong/runtime-core/task-definition';
import {
TaskSpecSemanticRegistry,
createBuiltInTaskSpecSemanticRegistry,
} from '@qinglong/runtime-core/task-spec-semantic';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredBoolean,
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
import { isPostgresAvailabilityError } from '../../connection/pool';
type Row = Record<string, unknown>;
type Queryable = Pick<PostgresQueryable, 'query'>;
export const CLUSTER_PLUGIN_PACKAGE_QUARANTINE_TARGET_LIMIT = 128;
function unavailable(
cause?: unknown,
): PluginPackageQuarantineUnavailableError {
return new PluginPackageQuarantineUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function same(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function mapStorageError(error: unknown): Error {
if (
error instanceof InvalidPluginPackageQuarantineError ||
error instanceof PluginPackageQuarantineConflictError ||
error instanceof PluginPackageQuarantineUnavailableError
) {
return error;
}
const state = postgresSqlState(error);
if (state === '23503' || state === '23505' || state === '23514') {
return new PluginPackageQuarantineConflictError(
'durable quarantine identity or target state conflicts',
);
}
return unavailable(error);
}
function recordJson(row: Row, key: string): Readonly<Record<string, unknown>> {
return postgresRequiredJsonObject(row[key], unavailable);
}
function integer(row: Row, key: string): number {
const value = postgresRequiredInteger(row[key], unavailable);
if (value < 0) throw unavailable();
return value;
}
function text(row: Row, key: string): string {
return postgresRequiredString(row[key], unavailable);
}
function taskRecord(row: Row): Readonly<TaskDefinitionRecord> {
try {
const description = row.description;
if (description !== null && typeof description !== 'string') {
throw unavailable();
}
return normalizeTaskDefinitionRecord({
projectId: text(row, 'projectId'),
taskId: text(row, 'taskId'),
revision: integer(row, 'revision'),
mutationId: text(row, 'mutationId'),
name: text(row, 'name'),
...(description === null ? {} : { description }),
kind: text(row, 'kind') as TaskDefinitionRecord['kind'],
spec: recordJson(
row,
'specJson',
) as unknown as TaskDefinitionRecord['spec'],
labels: recordJson(row, 'labelsJson') as TaskDefinitionRecord['labels'],
enabled: postgresRequiredBoolean(row.enabled, unavailable),
contentDigest: text(row, 'contentDigest'),
createdAtMs: integer(row, 'createdAtMs'),
updatedAtMs: integer(row, 'updatedAtMs'),
});
} catch (error) {
if (error instanceof PluginPackageQuarantineUnavailableError) throw error;
throw unavailable(error);
}
}
function activeSource(
contribution: Readonly<ProjectToolDefinitionSnapshotContribution>,
): Readonly<ProjectToolDefinitionSnapshotSource> {
return Object.freeze({
installationId: contribution.generation.installationId,
packageName: contribution.generation.packageName,
generation: contribution.generation.generation,
generationDigest: contribution.generation.generationDigest,
lockDigest: contribution.generation.lockDigest,
revisionDigest: contribution.revisionDigest,
});
}
export class PostgresPluginPackageQuarantineRepository
implements PluginPackageQuarantineRepository
{
readonly #registry: TaskSpecSemanticRegistry;
constructor(
private readonly pool: PostgresPool,
options: Readonly<{ registry?: TaskSpecSemanticRegistry }> = {},
) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError(
'PostgreSQL Plugin Package quarantine repository is invalid',
);
}
const registry =
options.registry ?? createBuiltInTaskSpecSemanticRegistry();
if (!(registry instanceof TaskSpecSemanticRegistry)) {
throw new TypeError('TaskSpec semantic registry is invalid');
}
this.#registry = registry;
}
async #eventByDigest(
queryable: Queryable,
eventDigest: string,
): Promise<Readonly<PluginPackageQuarantineEvent> | null> {
const result = await queryable.query<Row>(
`SELECT event_json AS "eventJson"
FROM "ql3"."plugin_package_quarantine_events"
WHERE event_digest = $1
LIMIT 2`,
[eventDigest],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
try {
return normalizePluginPackageQuarantineEvent(
recordJson(result.rows[0]!, 'eventJson') as unknown as
PluginPackageQuarantineEvent,
);
} catch (error) {
if (error instanceof PluginPackageQuarantineUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
async #receiptByEvent(
queryable: Queryable,
event: Readonly<PluginPackageQuarantineEvent>,
): Promise<Readonly<PluginPackageWithdrawalReceipt> | null> {
const result = await queryable.query<Row>(
`SELECT receipt_json AS "receiptJson"
FROM "ql3"."plugin_package_withdrawal_receipts"
WHERE event_digest = $1
LIMIT 2`,
[event.eventDigest],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
try {
const receipt = normalizePluginPackageWithdrawalReceipt(
recordJson(result.rows[0]!, 'receiptJson') as unknown as
PluginPackageWithdrawalReceipt,
);
assertPluginPackageWithdrawalMatchesEvent(event, receipt);
await this.#assertReceiptRelations(queryable, receipt);
return receipt;
} catch (error) {
if (error instanceof PluginPackageQuarantineUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
async #assertReceiptRelations(
queryable: Queryable,
receipt: Readonly<PluginPackageWithdrawalReceipt>,
): Promise<void> {
const tasks = await queryable.query<Row>(
`SELECT
item.project_id AS "projectId",
item.task_id AS "taskId",
item.previous_revision AS "previousRevision",
item.disabled_revision AS "disabledRevision",
item.previous_content_digest AS "previousContentDigest",
item.disabled_content_digest AS "disabledContentDigest",
previous.content_digest AS "storedPreviousContentDigest",
disabled.content_digest AS "storedDisabledContentDigest"
FROM "ql3"."plugin_package_withdrawal_tasks" AS item
JOIN "ql3"."task_definition_revisions" AS previous
ON previous.project_id = item.project_id
AND previous.task_id = item.task_id
AND previous.revision = item.previous_revision
JOIN "ql3"."task_definition_revisions" AS disabled
ON disabled.project_id = item.project_id
AND disabled.task_id = item.task_id
AND disabled.revision = item.disabled_revision
WHERE item.event_digest = $1
ORDER BY item.task_id`,
[receipt.eventDigest],
);
const withdrawals = Object.freeze(
tasks.rows.map((row) =>
Object.freeze({
taskId: text(row, 'taskId'),
previousRevision: integer(row, 'previousRevision'),
disabledRevision: integer(row, 'disabledRevision'),
previousContentDigest: text(row, 'previousContentDigest'),
disabledContentDigest: text(row, 'disabledContentDigest'),
}),
),
);
if (
tasks.rows.some(
(row) =>
text(row, 'projectId') !== receipt.target.projectId ||
text(row, 'previousContentDigest') !==
text(row, 'storedPreviousContentDigest') ||
text(row, 'disabledContentDigest') !==
text(row, 'storedDisabledContentDigest'),
) ||
!same(withdrawals, receipt.capability.taskWithdrawals)
) {
throw unavailable();
}
if (receipt.capability.status === 'not_active') return;
const snapshots = await queryable.query<Row>(
`SELECT snapshot_json AS "snapshotJson"
FROM "ql3"."project_tool_definition_snapshots"
WHERE project_id = $1 AND active_vector_digest = $2
AND snapshot_digest = $3
LIMIT 2`,
[
receipt.target.projectId,
receipt.capability.currentActiveVectorDigest,
receipt.capability.currentToolSnapshotDigest,
],
);
if (snapshots.rows.length !== 1) throw unavailable();
try {
const snapshot = normalizeProjectToolDefinitionSnapshot(
recordJson(snapshots.rows[0]!, 'snapshotJson') as unknown as
ProjectToolDefinitionSnapshot,
);
if (
snapshot.sources.length !== receipt.capability.retainedSourceCount ||
snapshot.sources.some(
(source) =>
source.packageName === receipt.target.packageName &&
source.installationId === receipt.target.installationId &&
source.lockDigest === receipt.target.lockDigest,
)
) {
throw unavailable();
}
} catch (error) {
if (error instanceof PluginPackageQuarantineUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
async #findStored(
queryable: Queryable,
eventDigest: string,
): Promise<Readonly<PluginPackageWithdrawalReceipt> | null> {
const event = await this.#eventByDigest(queryable, eventDigest);
if (!event) return null;
const receipt = await this.#receiptByEvent(queryable, event);
if (!receipt) throw unavailable();
return receipt;
}
async findTargetsByLockDigest(
lockDigest: string,
): Promise<readonly Readonly<PluginPackageQuarantineTarget>[]> {
if (typeof lockDigest !== 'string' || !/^[0-9a-f]{64}$/.test(lockDigest)) {
throw new InvalidPluginPackageQuarantineError('lockDigest is invalid');
}
try {
const result = await this.pool.query<Row>(
`SELECT record_json AS "recordJson"
FROM "ql3"."plugin_package_installs"
WHERE lock_digest = $1
AND state IN ('queued', 'staged', 'activating', 'active')
ORDER BY project_id, package_name, installation_id
LIMIT $2`,
[lockDigest, CLUSTER_PLUGIN_PACKAGE_QUARANTINE_TARGET_LIMIT + 1],
);
if (
result.rows.length > CLUSTER_PLUGIN_PACKAGE_QUARANTINE_TARGET_LIMIT
) {
throw new PluginPackageQuarantineConflictError(
'matching install targets exceed the Cluster limit',
);
}
return Object.freeze(
result.rows.map((row) => {
const record = normalizePluginPackageInstallRecord(
recordJson(row, 'recordJson') as unknown as
PluginPackageInstallRecord,
);
return Object.freeze({
projectId: record.projectId,
packageName: record.packageName,
installationId: record.installationId,
lockDigest: record.lockDigest,
installState:
record.state as PluginPackageQuarantineTarget['installState'],
installVersion: record.version,
installRecordDigest: record.recordDigest,
activeLockDigest: record.activeLockDigest,
});
}),
);
} catch (error) {
throw mapStorageError(error);
}
}
async findByEventDigest(
eventDigest: string,
): Promise<Readonly<PluginPackageWithdrawalReceipt> | null> {
if (
typeof eventDigest !== 'string' ||
!/^[0-9a-f]{64}$/.test(eventDigest)
) {
throw new InvalidPluginPackageQuarantineError('eventDigest is invalid');
}
try {
return await this.#findStored(this.pool, eventDigest);
} catch (error) {
throw mapStorageError(error);
}
}
async #install(
queryable: Queryable,
event: Readonly<PluginPackageQuarantineEvent>,
): Promise<Readonly<PluginPackageInstallRecord>> {
const result = await queryable.query<Row>(
`SELECT record_json AS "recordJson"
FROM "ql3"."plugin_package_installs"
WHERE installation_id = $1
LIMIT 2`,
[event.target.installationId],
);
if (result.rows.length !== 1) {
throw new PluginPackageQuarantineConflictError(
'target install is absent',
);
}
let record: Readonly<PluginPackageInstallRecord>;
try {
record = normalizePluginPackageInstallRecord(
recordJson(result.rows[0]!, 'recordJson') as unknown as
PluginPackageInstallRecord,
);
} catch (error) {
if (error instanceof PluginPackageQuarantineUnavailableError) {
throw error;
}
throw unavailable(error);
}
if (
record.projectId !== event.target.projectId ||
record.packageName !== event.target.packageName ||
record.lockDigest !== event.target.lockDigest ||
record.state !== event.target.installState ||
record.version !== event.target.installVersion ||
record.recordDigest !== event.target.installRecordDigest ||
record.activeLockDigest !== event.target.activeLockDigest
) {
throw new PluginPackageQuarantineConflictError(
'target install advanced or drifted',
);
}
return record;
}
async #activeContributions(
queryable: Queryable,
projectId: string,
): Promise<
readonly Readonly<ProjectToolDefinitionSnapshotContribution>[]
> {
const result = await queryable.query<Row>(
`SELECT revision.revision_json AS "revisionJson"
FROM "ql3"."plugin_package_install_heads" AS head
JOIN "ql3"."plugin_package_installs" AS head_install
ON head_install.installation_id = head.installation_id
JOIN "ql3"."plugin_package_installs" AS active_install
ON active_install.project_id = head.project_id
AND active_install.package_name = head.package_name
AND active_install.lock_digest = head_install.active_lock_digest
JOIN "ql3"."plugin_package_materialized_revisions" AS revision
ON revision.project_id = active_install.project_id
AND revision.package_name = active_install.package_name
AND revision.generation = active_install.target_generation
AND revision.lock_digest = active_install.lock_digest
LEFT JOIN "ql3"."plugin_package_quarantine_events" AS quarantine
ON quarantine.project_id = active_install.project_id
AND quarantine.package_name = active_install.package_name
AND quarantine.installation_id = active_install.installation_id
AND quarantine.lock_digest = active_install.lock_digest
WHERE head.project_id = $1
AND head_install.active_lock_digest IS NOT NULL
AND active_install.state = 'active'
AND quarantine.event_digest IS NULL
ORDER BY head.package_name
LIMIT $2`,
[projectId, MAX_PLUGIN_PACKAGE_QUARANTINE_RETAINED_SOURCES + 2],
);
if (
result.rows.length >
MAX_PLUGIN_PACKAGE_QUARANTINE_RETAINED_SOURCES + 1
) {
throw new PluginPackageQuarantineConflictError(
'active Package sources exceed the Cluster quarantine limit',
);
}
try {
return Object.freeze(
result.rows.map((row) =>
projectToolDefinitionSnapshotContribution(
recordJson(row, 'revisionJson') as never,
this.#registry,
),
),
);
} catch (error) {
if (error instanceof PluginPackageQuarantineUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
async #enabledOwnedTasks(
queryable: Queryable,
event: Readonly<PluginPackageQuarantineEvent>,
): Promise<readonly Readonly<TaskDefinitionRecord>[]> {
const result = await queryable.query<Row>(
`SELECT
head.project_id AS "projectId",
head.task_id AS "taskId",
revision.revision,
revision.mutation_id::text AS "mutationId",
revision.name,
revision.description,
revision.kind,
revision.spec_json AS "specJson",
revision.labels_json AS "labelsJson",
revision.enabled,
revision.content_digest AS "contentDigest",
head.created_at_ms AS "createdAtMs",
revision.created_at_ms AS "updatedAtMs"
FROM "ql3"."plugin_package_task_ownerships" AS ownership
JOIN "ql3"."task_definitions" AS head
ON head.project_id = ownership.project_id
AND head.task_id = ownership.task_id
JOIN "ql3"."task_definition_revisions" AS revision
ON revision.project_id = head.project_id
AND revision.task_id = head.task_id
AND revision.revision = head.current_revision
WHERE ownership.project_id = $1
AND ownership.package_name = $2
AND revision.enabled = true
ORDER BY ownership.task_id
LIMIT $3`,
[
event.target.projectId,
event.target.packageName,
MAX_PLUGIN_PACKAGE_QUARANTINE_TASK_WITHDRAWALS + 1,
],
);
if (
result.rows.length > MAX_PLUGIN_PACKAGE_QUARANTINE_TASK_WITHDRAWALS
) {
throw new PluginPackageQuarantineConflictError(
'owned Tasks exceed the quarantine withdrawal limit',
);
}
return Object.freeze(result.rows.map(taskRecord));
}
#disabledTask(
event: Readonly<PluginPackageQuarantineEvent>,
current: Readonly<TaskDefinitionRecord>,
committedAtMs: number,
): Readonly<{
disabled: Readonly<TaskDefinitionRecord>;
withdrawal: Readonly<PluginPackageQuarantineTaskWithdrawal>;
}> {
const disabled = createTaskDefinitionRecord(
{
projectId: current.projectId,
taskId: current.taskId,
expectedRevision: current.revision,
mutationId: pluginPackageQuarantineTaskMutationId(
event.eventDigest,
current.taskId,
),
name: current.name,
...(current.description === undefined
? {}
: { description: current.description }),
kind: current.kind,
spec: current.spec,
labels: current.labels,
enabled: false,
occurredAtMs: committedAtMs,
},
current.createdAtMs,
);
return Object.freeze({
disabled,
withdrawal: Object.freeze({
taskId: current.taskId,
previousRevision: current.revision,
disabledRevision: disabled.revision,
previousContentDigest: current.contentDigest,
disabledContentDigest: disabled.contentDigest,
}),
});
}
async #databaseNowMs(queryable: Queryable): Promise<number> {
const result = await queryable.query<Row>(
`SELECT floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint
AS "nowMs"`,
);
if (result.rows.length !== 1) throw unavailable();
return integer(result.rows[0]!, 'nowMs');
}
async #commit(
client: PostgresClient,
event: Readonly<PluginPackageQuarantineEvent>,
): Promise<
Readonly<{
created: boolean;
receipt: Readonly<PluginPackageWithdrawalReceipt>;
}>
> {
const existingEvent = await this.#eventByDigest(client, event.eventDigest);
if (existingEvent) {
if (!same(existingEvent, event)) {
throw new PluginPackageQuarantineConflictError(
'event digest is bound to another quarantine',
);
}
const existingReceipt = await this.#receiptByEvent(
client,
existingEvent,
);
if (!existingReceipt) throw unavailable();
return Object.freeze({ created: false, receipt: existingReceipt });
}
await this.#install(client, event);
const committedAtMs = Math.max(
await this.#databaseNowMs(client),
event.occurredAtMs,
);
let snapshot: Readonly<ProjectToolDefinitionSnapshot> | null = null;
let taskWrites: readonly Readonly<{
disabled: Readonly<TaskDefinitionRecord>;
withdrawal: Readonly<PluginPackageQuarantineTaskWithdrawal>;
}>[] = Object.freeze([]);
let receipt: Readonly<PluginPackageWithdrawalReceipt>;
if (event.target.installState !== 'active') {
receipt = createPluginPackageWithdrawalReceipt({
eventDigest: event.eventDigest,
target: event.target,
capability: {
status: 'not_active',
taskWithdrawals: [],
previousActiveVectorDigest: null,
currentActiveVectorDigest: null,
currentToolSnapshotDigest: null,
retainedSourceCount: 0,
},
committedAtMs,
});
} else {
const previousContributions = await this.#activeContributions(
client,
event.target.projectId,
);
const targetIndex = previousContributions.findIndex(
({ generation }) =>
generation.packageName === event.target.packageName &&
generation.installationId === event.target.installationId &&
generation.lockDigest === event.target.lockDigest,
);
if (targetIndex < 0) {
throw new PluginPackageQuarantineConflictError(
'active target is not a complete Tool source',
);
}
const previousSources = Object.freeze(
previousContributions.map(activeSource),
);
const retainedContributions = Object.freeze(
previousContributions.filter((_, index) => index !== targetIndex),
);
snapshot = createProjectToolDefinitionSnapshot({
projectId: event.target.projectId,
contributions: retainedContributions,
});
const tasks = await this.#enabledOwnedTasks(client, event);
taskWrites = Object.freeze(
tasks.map((task) => this.#disabledTask(event, task, committedAtMs)),
);
receipt = createPluginPackageWithdrawalReceipt({
eventDigest: event.eventDigest,
target: event.target,
capability: {
status: 'withdrawn',
taskWithdrawals: taskWrites.map(({ withdrawal }) => withdrawal),
previousActiveVectorDigest: projectToolDefinitionActiveVectorDigest(
event.target.projectId,
previousSources,
),
currentActiveVectorDigest: snapshot.activeVectorDigest,
currentToolSnapshotDigest: snapshot.snapshotDigest,
retainedSourceCount: snapshot.sources.length,
},
committedAtMs,
});
}
const committed = await client.query<Row>(
`SELECT "ql3"."commit_plugin_package_quarantine"(
$1::jsonb, $2::jsonb, $3::jsonb, $4::jsonb
) AS "created"`,
[
JSON.stringify(event),
JSON.stringify(receipt),
JSON.stringify(taskWrites),
JSON.stringify(snapshot),
],
);
if (
committed.rows.length !== 1 ||
typeof committed.rows[0]?.created !== 'boolean'
) {
throw unavailable();
}
const stored = await this.#findStored(client, event.eventDigest);
if (!stored || !same(stored, receipt)) throw unavailable();
return Object.freeze({
created: committed.rows[0].created,
receipt: stored,
});
}
async quarantine(
eventValue: Readonly<PluginPackageQuarantineEvent>,
confirmAuthorization: () => void | Promise<void>,
): Promise<
Readonly<{
status: 'created' | 'existing';
receipt: Readonly<PluginPackageWithdrawalReceipt>;
}>
> {
const event = normalizePluginPackageQuarantineEvent(eventValue);
if (typeof confirmAuthorization !== 'function') {
throw new InvalidPluginPackageQuarantineError(
'confirmAuthorization is invalid',
);
}
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
if (attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS) continue;
throw unavailable(error);
}
let began = false;
let authorizationFailure = false;
const authorize = async (): Promise<void> => {
try {
await confirmAuthorization();
} catch (error) {
authorizationFailure = true;
throw error;
}
};
try {
await configurePostgresDefinitionTransaction(client);
began = true;
await authorize();
const result = await this.#commit(client, event);
await authorize();
await client.query('COMMIT');
began = false;
return Object.freeze({
status: result.created ? 'created' : 'existing',
receipt: result.receipt,
});
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
if (authorizationFailure) throw error;
const state = postgresSqlState(error);
if (
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS &&
(isPostgresAvailabilityError(error) ||
!state ||
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) ||
state.startsWith('08'))
) {
continue;
}
throw mapStorageError(error);
} finally {
client.release();
}
}
throw unavailable();
}
}
@@ -0,0 +1,650 @@
// PostgreSQL adapter owned by Plugin Package publication and recovery.
import type {
PostgresClient,
PostgresPool,
} from '@qinglong/runtime-core';
import {
InvalidPluginPackageAutomationPublicationError,
MAX_PLUGIN_PACKAGE_AUTOMATION_PUBLICATION_BYTES,
PluginPackageAutomationPublicationConflictError,
PluginPackageAutomationPublicationUnavailableError,
assertPluginPackageAutomationPublicationRecoveryPageSize,
assertPluginPackageAutomationPublicationSuccessor,
normalizePluginPackageAutomationPublication,
normalizePluginPackageAutomationPublicationRecoveryCursor,
type PluginPackageAutomationPublication,
type PluginPackageAutomationPublicationRecoveryPage,
type PluginPackageAutomationPublicationRepository,
type PluginPackageAutomationPublicationRecoverySource,
type PluginPackageAutomationPublicationStartGuard,
} from '@qinglong/runtime-core/plugin-package-automation-publication';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
type Queryable = Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>;
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const PACKAGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
const DIGEST = /^[0-9a-f]{64}$/;
function invalid(message: string): never {
throw new InvalidPluginPackageAutomationPublicationError(message);
}
function unavailable(
error?: unknown,
): PluginPackageAutomationPublicationUnavailableError {
return new PluginPackageAutomationPublicationUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
function targetIdentity(projectId: unknown, packageName: unknown): {
readonly projectId: string;
readonly packageName: string;
} {
if (typeof projectId !== 'string' || !IDENTIFIER.test(projectId)) {
return invalid('projectId is invalid');
}
if (typeof packageName !== 'string' || !PACKAGE_NAME.test(packageName)) {
return invalid('packageName is invalid');
}
return { projectId, packageName };
}
function publicationDigest(value: unknown): string {
if (typeof value !== 'string' || !DIGEST.test(value)) {
return invalid('publicationDigest is invalid');
}
return value;
}
function serialize(
publication: Readonly<PluginPackageAutomationPublication>,
): string {
const value = JSON.stringify(publication);
if (
Buffer.byteLength(value, 'utf8') >
MAX_PLUGIN_PACKAGE_AUTOMATION_PUBLICATION_BYTES
) {
return invalid('publication exceeds the durable JSON budget');
}
return value;
}
function mapStorageError(error: unknown): Error {
if (
error instanceof InvalidPluginPackageAutomationPublicationError ||
error instanceof PluginPackageAutomationPublicationConflictError ||
error instanceof PluginPackageAutomationPublicationUnavailableError
) {
return error;
}
const state = postgresSqlState(error);
if (
state === '23503' ||
state === '23505' ||
state === '23514' ||
state === '40001'
) {
return new PluginPackageAutomationPublicationConflictError(
'durable publication chain changed',
);
}
return unavailable(error);
}
export class PostgresPluginPackageAutomationPublicationRepository
implements
PluginPackageAutomationPublicationRepository,
PluginPackageAutomationPublicationRecoverySource,
PluginPackageAutomationPublicationStartGuard
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError(
'PostgreSQL automation publication repository is invalid',
);
}
}
#parse(row: Row): Readonly<PluginPackageAutomationPublication> {
try {
const publication = normalizePluginPackageAutomationPublication(
postgresRequiredJsonObject(
row.publicationJson,
unavailable,
) as unknown as PluginPackageAutomationPublication,
);
if (
publication.publicationDigest !==
postgresRequiredString(row.publicationDigest, unavailable) ||
publication.target.projectId !==
postgresRequiredString(row.projectId, unavailable) ||
publication.target.packageName !==
postgresRequiredString(row.packageName, unavailable) ||
publication.target.installationId !==
postgresRequiredString(row.installationId, unavailable) ||
publication.target.lockDigest !==
postgresRequiredString(row.lockDigest, unavailable) ||
publication.target.generation !==
postgresRequiredInteger(row.generation, unavailable) ||
publication.target.generationDigest !==
postgresRequiredString(row.generationDigest, unavailable) ||
publication.target.materializedRevisionDigest !==
postgresRequiredString(row.materializedRevisionDigest, unavailable) ||
publication.state !==
postgresRequiredString(row.state, unavailable) ||
publication.version !==
postgresRequiredInteger(row.version, unavailable) ||
publication.publishedAtMs !==
postgresRequiredInteger(row.publishedAtMs, unavailable)
) {
throw unavailable();
}
return publication;
} catch (error) {
if (
error instanceof PluginPackageAutomationPublicationUnavailableError
) {
throw error;
}
throw unavailable(error);
}
}
async #findByDigest(
queryable: Queryable,
digest: string,
): Promise<Readonly<PluginPackageAutomationPublication> | null> {
const result = await queryable.query<Row>(
`SELECT publication_digest AS "publicationDigest",
project_id AS "projectId",
package_name AS "packageName",
installation_id AS "installationId",
lock_digest AS "lockDigest",
generation,
generation_digest AS "generationDigest",
materialized_revision_digest AS "materializedRevisionDigest",
state,
version,
published_at_ms AS "publishedAtMs",
publication_json AS "publicationJson"
FROM "ql3"."plugin_package_automation_publications"
WHERE publication_digest = $1
LIMIT 2`,
[digest],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return this.#parse(result.rows[0]!);
}
async #findCurrent(
queryable: Queryable,
projectId: string,
packageName: string,
lock = false,
): Promise<Readonly<PluginPackageAutomationPublication> | null> {
const result = await queryable.query<Row>(
`SELECT publication.publication_digest AS "publicationDigest",
publication.project_id AS "projectId",
publication.package_name AS "packageName",
publication.installation_id AS "installationId",
publication.lock_digest AS "lockDigest",
publication.generation,
publication.generation_digest AS "generationDigest",
publication.materialized_revision_digest
AS "materializedRevisionDigest",
publication.state,
publication.version,
publication.published_at_ms AS "publishedAtMs",
publication.publication_json AS "publicationJson"
FROM "ql3"."plugin_package_automation_publication_heads" AS head
JOIN "ql3"."plugin_package_automation_publications" AS publication
ON publication.publication_digest = head.publication_digest
WHERE head.project_id = $1 AND head.package_name = $2
LIMIT 2${lock ? ' FOR UPDATE OF head' : ''}`,
[projectId, packageName],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return this.#parse(result.rows[0]!);
}
async findCurrent(
projectIdValue: string,
packageNameValue: string,
): Promise<Readonly<PluginPackageAutomationPublication> | null> {
const { projectId, packageName } = targetIdentity(
projectIdValue,
packageNameValue,
);
try {
return await this.#findCurrent(this.pool, projectId, packageName);
} catch (error) {
throw mapStorageError(error);
}
}
async findByDigest(
publicationDigestValue: string,
): Promise<Readonly<PluginPackageAutomationPublication> | null> {
const digest = publicationDigest(publicationDigestValue);
try {
return await this.#findByDigest(this.pool, digest);
} catch (error) {
throw mapStorageError(error);
}
}
async isStartAllowed(
projectIdValue: string,
packageNameValue: string,
publicationDigestValue: string,
): Promise<boolean> {
const { projectId, packageName } = targetIdentity(
projectIdValue,
packageNameValue,
);
const digest = publicationDigest(publicationDigestValue);
try {
const result = await this.pool.query<Row>(
`SELECT "ql3"."plugin_package_automation_start_allowed"(
$1::varchar, $2::varchar, $3::char(64)
) AS "allowed"`,
[projectId, packageName, digest],
);
if (
result.rows.length !== 1 ||
typeof result.rows[0]?.allowed !== 'boolean'
) {
throw unavailable();
}
return result.rows[0].allowed;
} catch (error) {
throw mapStorageError(error);
}
}
async listPendingPage(options: {
readonly limit: number;
readonly after?: Readonly<{
readonly projectId: string;
readonly packageName: string;
}>;
}): Promise<Readonly<PluginPackageAutomationPublicationRecoveryPage>> {
if (!options || typeof options !== 'object' || Array.isArray(options)) {
throw new InvalidPluginPackageAutomationPublicationError(
'pending page options are invalid',
);
}
assertPluginPackageAutomationPublicationRecoveryPageSize(options.limit);
const after =
options.after === undefined
? undefined
: normalizePluginPackageAutomationPublicationRecoveryCursor(
options.after,
);
try {
const result = await this.pool.query<Row>(
`SELECT install.project_id AS "projectId",
install.package_name AS "packageName"
FROM "ql3"."plugin_package_install_heads" AS install_head
JOIN "ql3"."plugin_package_installs" AS install
ON install.installation_id = install_head.installation_id
JOIN "ql3"."plugin_package_materialized_revisions" AS revision
ON revision.project_id = install.project_id
AND revision.package_name = install.package_name
AND revision.generation = install.target_generation
AND revision.lock_digest = install.lock_digest
LEFT JOIN
"ql3"."plugin_package_automation_publication_heads" AS publication
ON publication.project_id = install.project_id
AND publication.package_name = install.package_name
WHERE install.state = 'active'
AND install.active_lock_digest = install.lock_digest
AND NOT EXISTS (
SELECT 1
FROM "ql3"."plugin_package_quarantine_events" AS quarantine
WHERE quarantine.project_id = install.project_id
AND quarantine.package_name = install.package_name
AND quarantine.installation_id = install.installation_id
AND quarantine.lock_digest = install.lock_digest
)
AND (
publication.generation_digest IS NULL OR
publication.generation_digest <> revision.generation_digest
)
AND NOT 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 = install.installation_id
AND provenance.lock_digest = install.lock_digest
)
AND (
$1::varchar IS NULL OR install.project_id > $1 OR
(install.project_id = $1 AND install.package_name > $2)
)
ORDER BY install.project_id, install.package_name
LIMIT $3`,
[
after?.projectId ?? null,
after?.packageName ?? null,
options.limit + 1,
],
);
const truncated = result.rows.length > options.limit;
const candidates = result.rows.slice(0, options.limit).map((row) =>
Object.freeze({
projectId: postgresRequiredString(row.projectId, unavailable),
packageName: postgresRequiredString(row.packageName, unavailable),
}),
);
const last = candidates.at(-1);
return Object.freeze({
candidates: Object.freeze(candidates),
truncated,
...(truncated && last
? {
next: Object.freeze({
projectId: last.projectId,
packageName: last.packageName,
}),
}
: {}),
});
} catch (error) {
throw mapStorageError(error);
}
}
async findCurrentInTransaction(
client: PostgresClient,
projectIdValue: string,
packageNameValue: string,
): Promise<Readonly<PluginPackageAutomationPublication> | null> {
const { projectId, packageName } = targetIdentity(
projectIdValue,
packageNameValue,
);
if (!client || typeof client.query !== 'function') {
throw new TypeError(
'PostgreSQL automation publication transaction is invalid',
);
}
return this.#findCurrent(client, projectId, packageName, true);
}
async publishInTransaction(
client: PostgresClient,
value: Readonly<PluginPackageAutomationPublication>,
): Promise<
Readonly<{
status: 'created' | 'existing';
publication: Readonly<PluginPackageAutomationPublication>;
}>
> {
if (!client || typeof client.query !== 'function') {
throw new TypeError(
'PostgreSQL automation publication transaction is invalid',
);
}
const publication = normalizePluginPackageAutomationPublication(value);
const publicationJson = serialize(publication);
const existing = await this.#findByDigest(
client,
publication.publicationDigest,
);
if (existing) {
if (serialize(existing) !== publicationJson) {
throw new PluginPackageAutomationPublicationConflictError(
'publication digest is bound to another semantic publication',
);
}
return Object.freeze({
status: 'existing' as const,
publication: existing,
});
}
const current = await this.#findCurrent(
client,
publication.target.projectId,
publication.target.packageName,
true,
);
if (publication.version === 1) {
if (current) {
throw new PluginPackageAutomationPublicationConflictError(
'Package already has an automation publication head',
);
}
} else {
if (!current) {
throw new PluginPackageAutomationPublicationConflictError(
'previous automation publication head is absent',
);
}
try {
assertPluginPackageAutomationPublicationSuccessor(
current,
publication,
);
} catch (error) {
if (error instanceof InvalidPluginPackageAutomationPublicationError) {
throw new PluginPackageAutomationPublicationConflictError(
'automation publication does not succeed the current head',
);
}
throw error;
}
}
const revision = await client.query<Row>(
`SELECT revision_digest AS "revisionDigest",
project_id AS "projectId",
package_name AS "packageName",
generation,
lock_digest AS "lockDigest"
FROM "ql3"."plugin_package_materialized_revisions"
WHERE generation_digest = $1
LIMIT 2`,
[publication.target.generationDigest],
);
if (
revision.rows.length !== 1 ||
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]!.lockDigest, unavailable) !==
publication.target.lockDigest
) {
throw new PluginPackageAutomationPublicationConflictError(
'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',
);
}
await client.query(
`INSERT INTO "ql3"."plugin_package_automation_publications" (
publication_digest, project_id, package_name, installation_id,
lock_digest, generation, generation_digest,
materialized_revision_digest, state, version,
previous_publication_digest, lifecycle_event_digest,
published_at_ms, publication_json
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
$14::jsonb
)`,
[
publication.publicationDigest,
publication.target.projectId,
publication.target.packageName,
publication.target.installationId,
publication.target.lockDigest,
publication.target.generation,
publication.target.generationDigest,
publication.target.materializedRevisionDigest,
publication.state,
publication.version,
publication.previousPublicationDigest,
publication.lifecycleEventDigest,
publication.publishedAtMs,
publicationJson,
],
);
if (!current) {
await client.query(
`INSERT INTO "ql3"."plugin_package_automation_publication_heads" (
project_id, package_name, publication_digest,
generation_digest, state, version, updated_at_ms
) VALUES ($1, $2, $3, $4, $5, $6, $7)`,
[
publication.target.projectId,
publication.target.packageName,
publication.publicationDigest,
publication.target.generationDigest,
publication.state,
publication.version,
publication.publishedAtMs,
],
);
} else {
const updated = await client.query(
`UPDATE "ql3"."plugin_package_automation_publication_heads"
SET publication_digest = $1, generation_digest = $2, state = $3,
version = $4, updated_at_ms = $5
WHERE project_id = $6 AND package_name = $7
AND publication_digest = $8 AND version = $9`,
[
publication.publicationDigest,
publication.target.generationDigest,
publication.state,
publication.version,
publication.publishedAtMs,
publication.target.projectId,
publication.target.packageName,
current.publicationDigest,
current.version,
],
);
if (updated.rowCount !== 1) {
throw new PluginPackageAutomationPublicationConflictError(
'automation publication head changed',
);
}
}
return Object.freeze({
status: 'created' as const,
publication,
});
}
async publish(
value: Readonly<PluginPackageAutomationPublication>,
): Promise<
Readonly<{
status: 'created' | 'existing';
publication: Readonly<PluginPackageAutomationPublication>;
}>
> {
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
if (attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS) continue;
throw unavailable(error);
}
let began = false;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const result = await this.publishInTransaction(client, value);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS &&
(state === undefined ||
state.startsWith('08') ||
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state))
) {
continue;
}
throw mapStorageError(error);
} finally {
client.release();
}
}
throw unavailable();
}
}
@@ -0,0 +1,598 @@
// PostgreSQL adapter owned by Plugin Package publication and recovery.
import type {
PostgresClient,
PostgresPool,
} from '@qinglong/runtime-core';
import {
InvalidPluginPackageTaskReconciliationError,
PluginPackageTaskReconciliationConflictError,
PluginPackageTaskReconciliationUnavailableError,
normalizePluginPackageTaskReconciliationReceipt,
planPluginPackageTaskReconciliation,
pluginPackageTaskReconciliationTaskIds,
type PluginPackageTaskOwnershipFact,
type PluginPackageTaskReconciliationReceipt,
type PluginPackageTaskReconciliationRepository,
} from '@qinglong/runtime-core/plugin-package-task-reconciliation';
import {
assertPluginPackageTaskPublicationRecoveryPageSize,
normalizePluginPackageTaskPublicationRecoveryCursor,
type PluginPackageTaskPublicationRecoveryPage,
type PluginPackageTaskPublicationRecoverySource,
} from '@qinglong/runtime-core/plugin-package-task-publication';
import {
normalizePluginPackageMaterializedRevision,
type PluginPackageMaterializedRevision,
} from '@qinglong/runtime-core/plugin-package-resource-materialization';
import {
normalizePluginPackageResourceGeneration,
type PluginPackageResourceGenerationSource,
} from '@qinglong/runtime-core/plugin-package-resource-generation';
import {
normalizeTaskDefinitionRecord,
type TaskDefinitionRecord,
} from '@qinglong/runtime-core/task-definition';
import {
compileClusterCommandTaskDefinition,
type ClusterTaskExecutionRevision,
} from '@qinglong/runtime-core/cluster-execution-revision';
import {
BUILT_IN_COMMAND_TASK_SPEC_SCHEMA,
TaskSpecSemanticRegistry,
createBuiltInTaskSpecSemanticRegistry,
} from '@qinglong/runtime-core/task-spec-semantic';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredBoolean,
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
const SELECT_TASK_FIELDS = `
head.project_id AS "projectId",
head.task_id AS "taskId",
revision.revision,
revision.mutation_id AS "mutationId",
revision.name,
revision.description,
revision.kind,
revision.spec_json AS "specJson",
revision.labels_json AS "labelsJson",
revision.enabled,
revision.content_digest AS "contentDigest",
head.created_at_ms AS "createdAtMs",
revision.created_at_ms AS "updatedAtMs"`;
function unavailable(): PluginPackageTaskReconciliationUnavailableError {
return new PluginPackageTaskReconciliationUnavailableError();
}
function taskRecord(row: Row): Readonly<TaskDefinitionRecord> {
try {
const description = row.description;
if (description !== null && typeof description !== 'string') {
throw unavailable();
}
return normalizeTaskDefinitionRecord({
projectId: postgresRequiredString(row.projectId, unavailable),
taskId: postgresRequiredString(row.taskId, unavailable),
revision: postgresRequiredInteger(row.revision, unavailable),
mutationId: postgresRequiredString(row.mutationId, unavailable),
name: postgresRequiredString(row.name, unavailable),
...(description === null ? {} : { description }),
kind: postgresRequiredString(
row.kind,
unavailable,
) as TaskDefinitionRecord['kind'],
spec: postgresRequiredJsonObject(
row.specJson,
unavailable,
) as unknown as TaskDefinitionRecord['spec'],
labels: postgresRequiredJsonObject(
row.labelsJson,
unavailable,
) as TaskDefinitionRecord['labels'],
enabled: postgresRequiredBoolean(row.enabled, unavailable),
contentDigest: postgresRequiredString(row.contentDigest, unavailable),
createdAtMs: postgresRequiredInteger(row.createdAtMs, unavailable),
updatedAtMs: postgresRequiredInteger(row.updatedAtMs, unavailable),
});
} catch (error) {
if (error instanceof PluginPackageTaskReconciliationUnavailableError) {
throw error;
}
throw unavailable();
}
}
function executionPlanJson(
value: Readonly<ClusterTaskExecutionRevision>,
): Readonly<Record<string, unknown>> {
return Object.freeze({
command: value.command,
environment: value.environment,
...(value.workingDirectory === undefined
? {}
: { workingDirectory: value.workingDirectory }),
...(value.timeoutMs === undefined ? {} : { timeoutMs: value.timeoutMs }),
...(value.placement === undefined
? {}
: { placement: value.placement }),
});
}
function executionJson(
value: Readonly<ClusterTaskExecutionRevision>,
): Readonly<Record<string, unknown>> {
return Object.freeze({
projectId: value.projectId,
taskId: value.taskId,
sourceRevision: value.sourceRevision,
taskRevision: value.taskRevision,
sourceContentDigest: value.sourceContentDigest,
executorType: value.executorType,
planSchema: value.planSchema,
planJson: executionPlanJson(value),
contentDigest: value.contentDigest,
createdAtMs: value.createdAtMs,
});
}
function mappedError(error: unknown): Error {
if (
error instanceof InvalidPluginPackageTaskReconciliationError ||
error instanceof PluginPackageTaskReconciliationConflictError ||
error instanceof PluginPackageTaskReconciliationUnavailableError
) {
return error;
}
const state = postgresSqlState(error);
if (
state === '23503' ||
state === '23505' ||
state === '23514' ||
state === '40001'
) {
return new PluginPackageTaskReconciliationConflictError(
'durable generation or TaskDefinition fence changed',
);
}
return new PluginPackageTaskReconciliationUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
export class PostgresPluginPackageTaskReconciliationRepository
implements
PluginPackageTaskReconciliationRepository,
PluginPackageTaskPublicationRecoverySource
{
readonly #registry: TaskSpecSemanticRegistry;
constructor(
private readonly pool: PostgresPool,
registry = createBuiltInTaskSpecSemanticRegistry(),
) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function' ||
!(registry instanceof TaskSpecSemanticRegistry)
) {
throw new TypeError(
'PostgreSQL Package Task reconciliation repository options are invalid',
);
}
this.#registry = registry;
}
async #findStored(
queryable: Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>,
generationDigest: string,
): Promise<Readonly<PluginPackageTaskReconciliationReceipt> | null> {
const result = await queryable.query<Row>(
`SELECT receipt_json AS "receiptJson"
FROM "ql3"."plugin_package_task_reconciliations"
WHERE generation_digest = $1
LIMIT 2`,
[generationDigest],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
try {
return normalizePluginPackageTaskReconciliationReceipt(
postgresRequiredJsonObject(
result.rows[0]!.receiptJson,
unavailable,
) as unknown as PluginPackageTaskReconciliationReceipt,
);
} catch (error) {
if (error instanceof PluginPackageTaskReconciliationUnavailableError) {
throw error;
}
throw unavailable();
}
}
async find(
generationDigest: string,
): Promise<Readonly<PluginPackageTaskReconciliationReceipt> | null> {
if (
typeof generationDigest !== 'string' ||
!/^[0-9a-f]{64}$/.test(generationDigest)
) {
throw new InvalidPluginPackageTaskReconciliationError(
'generationDigest is invalid',
);
}
try {
return await this.#findStored(this.pool, generationDigest);
} catch (error) {
throw mappedError(error);
}
}
async listPendingPage(options: {
readonly limit: number;
readonly after?: Readonly<{
readonly projectId: string;
readonly packageName: string;
}>;
}): Promise<Readonly<PluginPackageTaskPublicationRecoveryPage>> {
if (!options || typeof options !== 'object' || Array.isArray(options)) {
throw new InvalidPluginPackageTaskReconciliationError(
'pending page options are invalid',
);
}
assertPluginPackageTaskPublicationRecoveryPageSize(options.limit);
const after =
options.after === undefined
? undefined
: normalizePluginPackageTaskPublicationRecoveryCursor(options.after);
try {
const result = await this.pool.query<Row>(
`SELECT head.project_id AS "projectId",
head.package_name AS "packageName"
FROM "ql3"."plugin_package_install_heads" AS head
JOIN "ql3"."plugin_package_installs" AS install
ON install.installation_id = head.installation_id
LEFT JOIN "ql3"."plugin_package_task_reconciliations" AS receipt
ON receipt.project_id = install.project_id
AND receipt.package_name = install.package_name
AND receipt.generation = install.target_generation
AND receipt.lock_digest = install.lock_digest
WHERE install.state = 'active'
AND install.active_lock_digest = install.lock_digest
AND receipt.generation_digest IS NULL
AND NOT 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 = install.installation_id
AND provenance.lock_digest = install.lock_digest
)
AND (
$1::varchar IS NULL OR head.project_id > $1 OR
(head.project_id = $1 AND head.package_name > $2)
)
ORDER BY head.project_id, head.package_name
LIMIT $3`,
[
after?.projectId ?? null,
after?.packageName ?? null,
options.limit + 1,
],
);
const truncated = result.rows.length > options.limit;
const candidates = result.rows.slice(0, options.limit).map((row) =>
Object.freeze({
projectId: postgresRequiredString(row.projectId, unavailable),
packageName: postgresRequiredString(row.packageName, unavailable),
}),
);
const last = candidates.at(-1);
return Object.freeze({
candidates: Object.freeze(candidates),
truncated,
...(truncated && last
? {
next: Object.freeze({
projectId: last.projectId,
packageName: last.packageName,
}),
}
: {}),
});
} catch (error) {
throw mappedError(error);
}
}
async reconcile(
revisionValue: Readonly<PluginPackageMaterializedRevision>,
activeGenerationSource: PluginPackageResourceGenerationSource,
): Promise<
Readonly<{
status: 'created' | 'existing';
receipt: Readonly<PluginPackageTaskReconciliationReceipt>;
}>
> {
const revision = normalizePluginPackageMaterializedRevision(
revisionValue,
this.#registry,
);
if (
!activeGenerationSource ||
typeof activeGenerationSource.findActiveResourceGeneration !== 'function'
) {
throw new InvalidPluginPackageTaskReconciliationError(
'active generation source is invalid',
);
}
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch {
throw unavailable();
}
let began = false;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const existing = await this.#findStored(
client,
revision.generation.generationDigest,
);
if (existing) {
if (
existing.materializedRevisionDigest !== revision.revisionDigest ||
existing.projectId !== revision.generation.projectId ||
existing.packageName !== revision.generation.packageName
) {
throw new PluginPackageTaskReconciliationConflictError(
'generation is bound to another materialized revision',
);
}
await client.query('COMMIT');
began = false;
return Object.freeze({
status: 'existing',
receipt: existing,
});
}
const materialized = await client.query<Row>(
`SELECT revision_digest AS "revisionDigest"
FROM "ql3"."plugin_package_materialized_revisions"
WHERE generation_digest = $1`,
[revision.generation.generationDigest],
);
if (
materialized.rows.length !== 1 ||
postgresRequiredString(
materialized.rows[0]!.revisionDigest,
unavailable,
) !== revision.revisionDigest
) {
throw new PluginPackageTaskReconciliationConflictError(
'materialized revision is not durably published',
);
}
const install = await client.query<Row>(
`SELECT install.installation_id AS "installationId",
install.state,
install.target_generation AS "targetGeneration",
install.lock_digest AS "lockDigest",
install.previous_active_lock_digest AS "previousLockDigest"
FROM "ql3"."plugin_package_install_heads" AS head
JOIN "ql3"."plugin_package_installs" AS install
ON install.installation_id = head.installation_id
WHERE head.project_id = $1 AND head.package_name = $2
AND NOT 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 = install.installation_id
AND provenance.lock_digest = install.lock_digest
)
FOR UPDATE OF install`,
[
revision.generation.projectId,
revision.generation.packageName,
],
);
const installRow = install.rows[0];
if (
install.rows.length !== 1 ||
!installRow ||
postgresRequiredString(installRow.installationId, unavailable) !==
revision.generation.installationId ||
postgresRequiredString(installRow.state, unavailable) !== 'active' ||
postgresRequiredInteger(installRow.targetGeneration, unavailable) !==
revision.generation.generation ||
postgresRequiredString(installRow.lockDigest, unavailable) !==
revision.generation.lockDigest ||
(installRow.previousLockDigest === null
? null
: postgresRequiredString(
installRow.previousLockDigest,
unavailable,
)) !== revision.generation.previousActiveLockDigest
) {
throw new PluginPackageTaskReconciliationConflictError(
'Package install head is not the materialized generation',
);
}
const previousResult =
revision.generation.generation === 1
? { rows: [] as Row[] }
: await client.query<Row>(
`SELECT receipt_json AS "receiptJson"
FROM "ql3"."plugin_package_task_reconciliations"
WHERE project_id = $1 AND package_name = $2
AND generation = $3`,
[
revision.generation.projectId,
revision.generation.packageName,
revision.generation.generation - 1,
],
);
const previous =
previousResult.rows.length === 0
? null
: normalizePluginPackageTaskReconciliationReceipt(
postgresRequiredJsonObject(
previousResult.rows[0]!.receiptJson,
unavailable,
) as unknown as PluginPackageTaskReconciliationReceipt,
);
if (
previousResult.rows.length > 1 ||
(revision.generation.generation > 1 && previous === null)
) {
throw new PluginPackageTaskReconciliationConflictError(
'previous generation receipt is missing',
);
}
const taskIds = pluginPackageTaskReconciliationTaskIds(
revision,
previous,
this.#registry,
);
const facts: PluginPackageTaskOwnershipFact[] = [];
for (const taskId of taskIds) {
const result = await client.query<Row>(
`SELECT
${SELECT_TASK_FIELDS},
ownership.package_name AS "ownerPackageName"
FROM (SELECT 1) AS seed
LEFT JOIN "ql3"."task_definitions" AS head
ON head.project_id = $1 AND head.task_id = $2
LEFT JOIN "ql3"."task_definition_revisions" AS revision
ON revision.project_id = head.project_id
AND revision.task_id = head.task_id
AND revision.revision = head.current_revision
LEFT JOIN "ql3"."plugin_package_task_ownerships" AS ownership
ON ownership.project_id = $1 AND ownership.task_id = $2`,
[revision.generation.projectId, taskId],
);
if (result.rows.length !== 1) throw unavailable();
const row = result.rows[0]!;
facts.push(
Object.freeze({
taskId,
packageName:
row.ownerPackageName === null
? null
: postgresRequiredString(
row.ownerPackageName,
unavailable,
),
current: row.revision === null ? null : taskRecord(row),
}),
);
}
const clock = await client.query<Row>(
`SELECT floor(
extract(epoch FROM clock_timestamp()) * 1000
)::bigint AS "nowMs"`,
);
if (clock.rows.length !== 1) throw unavailable();
const plan = planPluginPackageTaskReconciliation({
revision,
previousReceipt: previous,
facts: Object.freeze(facts),
committedAtMs: postgresRequiredInteger(
clock.rows[0]!.nowMs,
unavailable,
),
taskSpecSemanticRegistry: this.#registry,
});
const activeValue =
await activeGenerationSource.findActiveResourceGeneration(
revision.generation.projectId,
revision.generation.packageName,
);
if (
activeValue === null ||
normalizePluginPackageResourceGeneration(activeValue)
.generationDigest !== revision.generation.generationDigest
) {
throw new PluginPackageTaskReconciliationConflictError(
'active generation changed during reconciliation',
);
}
const executions = plan.writes.flatMap(({ definition }) =>
definition.enabled &&
definition.kind === 'command' &&
definition.spec.schema === BUILT_IN_COMMAND_TASK_SPEC_SCHEMA
? [
executionJson(
compileClusterCommandTaskDefinition(
definition,
this.#registry,
),
),
]
: [],
);
const committed = await client.query<Row>(
`SELECT "ql3"."commit_plugin_package_task_reconciliation"(
$1::char(64), $2::char(64), $3::jsonb, $4::jsonb, $5::jsonb
) AS "created"`,
[
revision.generation.generationDigest,
revision.revisionDigest,
JSON.stringify(plan.receipt),
JSON.stringify(plan.writes),
JSON.stringify(executions),
],
);
if (
committed.rows.length !== 1 ||
typeof committed.rows[0]!.created !== 'boolean'
) {
throw unavailable();
}
await client.query('COMMIT');
began = false;
return Object.freeze({
status: committed.rows[0]!.created ? 'created' : 'existing',
receipt: plan.receipt,
});
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
state &&
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
) {
continue;
}
throw mappedError(error);
} finally {
client.release();
}
}
throw unavailable();
}
}
@@ -0,0 +1,484 @@
// PostgreSQL Plugin Package publisher revocation proposal authority.
import type {
PostgresClient,
PostgresPool,
} from '@qinglong/runtime-core';
import {
approvalRequestDigest,
normalizeApprovalRequestRecord,
type ApprovalRequestRecord,
} from '@qinglong/runtime-core/approved-action';
import {
PluginPackagePublisherRevocationProposalConflictError,
PluginPackagePublisherRevocationProposalUnavailableError,
normalizePluginPackagePublisherRevocationProposal,
type CreatePluginPackagePublisherRevocationProposalCommand,
type CreatePluginPackagePublisherRevocationProposalResult,
type PluginPackagePublisherRevocationProposal,
type PluginPackagePublisherRevocationProposalRepository,
} from '@qinglong/runtime-core/plugin-package-publisher-revocation-proposal';
import {
normalizeSecurityAuditRecord,
type SecurityAuditRecord,
} from '@qinglong/runtime-core/security-audit';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredBoolean,
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
type Queryable = Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>;
// Must stay byte-for-byte compatible with publisher provenance mutations.
const SIGNER_ADVISORY_LOCK_SEED = 774635229;
async function publisherSignerLock(
queryable: Queryable,
publisher: string,
keyId: string,
): Promise<void> {
await queryable.query(
`SELECT pg_advisory_xact_lock(hashtextextended($1, $2))`,
[JSON.stringify([publisher, keyId]), SIGNER_ADVISORY_LOCK_SEED],
);
}
function unavailable(
cause?: unknown,
): PluginPackagePublisherRevocationProposalUnavailableError {
return new PluginPackagePublisherRevocationProposalUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function nullableString(value: unknown): string | null {
return value === null ? null : postgresRequiredString(value, unavailable);
}
function nullableInteger(value: unknown): number | null {
return value === null ? null : postgresRequiredInteger(value, unavailable);
}
function same(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function mappedError(error: unknown): Error {
if (
error instanceof PluginPackagePublisherRevocationProposalConflictError ||
error instanceof PluginPackagePublisherRevocationProposalUnavailableError
) {
return error;
}
const state = postgresSqlState(error);
if (state === '23503' || state === '23505' || state === '23514') {
return new PluginPackagePublisherRevocationProposalConflictError();
}
return unavailable(error);
}
function parseProposal(
row: Row,
): Readonly<PluginPackagePublisherRevocationProposal> {
try {
const proposal = normalizePluginPackagePublisherRevocationProposal(
postgresRequiredJsonObject(
row.proposalJson,
unavailable,
) as unknown as PluginPackagePublisherRevocationProposal,
);
if (
proposal.proposalDigest !==
postgresRequiredString(row.proposalDigest, unavailable)
) {
throw unavailable();
}
return proposal;
} catch (error) {
if (
error instanceof
PluginPackagePublisherRevocationProposalUnavailableError
) {
throw error;
}
throw unavailable(error);
}
}
function parseApprovalRequest(
row: Row,
): Readonly<ApprovalRequestRecord> {
try {
const request = normalizeApprovalRequestRecord(
postgresRequiredJsonObject(
row.requestJson,
unavailable,
) as unknown as ApprovalRequestRecord,
);
if (
approvalRequestDigest(request) !==
postgresRequiredString(row.requestDigest, unavailable)
) {
throw unavailable();
}
return request;
} catch (error) {
if (
error instanceof
PluginPackagePublisherRevocationProposalUnavailableError
) {
throw error;
}
throw unavailable(error);
}
}
function parseAudit(row: Row): Readonly<SecurityAuditRecord> {
try {
const subjectType = nullableString(row.subjectType);
const subjectId = nullableString(row.subjectId);
const projectVersion = nullableInteger(row.projectVersion);
if (!Array.isArray(row.reasons)) throw unavailable();
return normalizeSecurityAuditRecord({
eventId: postgresRequiredString(row.eventId, unavailable),
requestId: postgresRequiredString(row.requestId, unavailable),
operationId: postgresRequiredString(row.operationId, unavailable),
projectId: nullableString(row.projectId),
subject:
subjectType === null || subjectId === null
? null
: { type: subjectType, id: subjectId },
authenticationId: nullableString(row.authenticationId),
outcome: postgresRequiredString(row.outcome, unavailable),
reasons: row.reasons,
fence:
projectVersion === null
? null
: {
projectVersion,
bindingVersion: nullableInteger(row.bindingVersion),
},
occurredAtMs: postgresRequiredInteger(row.occurredAtMs, unavailable),
} as SecurityAuditRecord);
} catch (error) {
if (
error instanceof
PluginPackagePublisherRevocationProposalUnavailableError
) {
throw error;
}
throw unavailable(error);
}
}
async function proposalByActionRef(
queryable: Queryable,
actionRef: string,
): Promise<Readonly<PluginPackagePublisherRevocationProposal> | null> {
const result = await queryable.query<Row>(
`SELECT proposal_json AS "proposalJson",
proposal_digest AS "proposalDigest"
FROM "ql3"."plugin_package_publisher_revocation_proposals"
WHERE action_ref = $1
LIMIT 2`,
[actionRef],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return parseProposal(result.rows[0]!);
}
async function auditById(
queryable: Queryable,
eventId: string,
): Promise<Readonly<SecurityAuditRecord> | null> {
const result = await queryable.query<Row>(
`SELECT event_id AS "eventId", request_id AS "requestId",
operation_id AS "operationId", project_id AS "projectId",
subject_type AS "subjectType", subject_id AS "subjectId",
authentication_id AS "authenticationId", outcome,
reasons, project_version AS "projectVersion",
binding_version AS "bindingVersion",
occurred_at_ms AS "occurredAtMs"
FROM "ql3"."security_audit_events"
WHERE event_id = $1
LIMIT 2`,
[eventId],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return parseAudit(result.rows[0]!);
}
export function findPostgresPluginPackagePublisherRevocationProposal(
queryable: Queryable,
actionRef: string,
): Promise<Readonly<PluginPackagePublisherRevocationProposal> | null> {
return proposalByActionRef(queryable, actionRef);
}
export class PostgresPluginPackagePublisherRevocationProposalRepository
implements PluginPackagePublisherRevocationProposalRepository
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError(
'PostgreSQL publisher revocation proposal pool is invalid',
);
}
}
async #transaction<T>(
work: (client: PostgresClient) => Promise<T>,
): Promise<T> {
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
throw mappedError(error);
}
let began = false;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const result = await work(client);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
state &&
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
) {
continue;
}
throw mappedError(error);
} finally {
client.release();
}
}
throw unavailable();
}
async findProposalByActionRef(
actionRef: string,
): Promise<Readonly<PluginPackagePublisherRevocationProposal> | null> {
try {
return await proposalByActionRef(this.pool, actionRef);
} catch (error) {
throw mappedError(error);
}
}
async listApprovedRequests(
limitValue: number,
): Promise<readonly Readonly<ApprovalRequestRecord>[]> {
if (
!Number.isSafeInteger(limitValue) ||
limitValue < 1 ||
limitValue > 64
) {
throw new TypeError(
'publisher revocation approval page limit is invalid',
);
}
try {
const result = await this.pool.query<Row>(
`SELECT request.request_json AS "requestJson",
request.request_digest AS "requestDigest"
FROM "ql3"."approval_requests" AS request
JOIN "ql3"."plugin_package_publisher_revocation_proposals"
AS proposal
ON proposal.action_ref = request.action_ref
WHERE request.state = 'approved'
AND request.action_type =
'plugin_package.publisher_key.revoke'
ORDER BY request.updated_at_ms, request.request_id
LIMIT $1`,
[limitValue],
);
if (result.rows.length > limitValue) throw unavailable();
return Object.freeze(result.rows.map(parseApprovalRequest));
} catch (error) {
throw mappedError(error);
}
}
createProposal(
command: CreatePluginPackagePublisherRevocationProposalCommand,
): Promise<
Readonly<CreatePluginPackagePublisherRevocationProposalResult>
> {
const proposal = normalizePluginPackagePublisherRevocationProposal(
command.proposal,
);
const audit = normalizeSecurityAuditRecord(command.audit);
if (
audit.requestId !== proposal.actionRef ||
audit.operationId !==
'plugin_package.publisher_revocation.propose' ||
audit.projectId !== proposal.projectId ||
audit.subject?.type !== proposal.proposedBy.type ||
audit.subject.id !== proposal.proposedBy.id ||
audit.authenticationId === null ||
audit.outcome !== 'allowed' ||
!same(audit.reasons, ['publisher_revocation_proposal']) ||
audit.fence?.projectVersion !==
proposal.proposalFence.projectVersion ||
audit.fence.bindingVersion !==
proposal.proposalFence.bindingVersion ||
audit.occurredAtMs !== proposal.createdAtMs
) {
throw new PluginPackagePublisherRevocationProposalConflictError();
}
return this.#transaction(async (client) => {
const existing = await proposalByActionRef(
client,
proposal.actionRef,
);
if (existing) {
const existingAudit = await auditById(client, audit.eventId);
if (
!same(existing, proposal) ||
!existingAudit ||
!same(existingAudit, audit)
) {
throw new PluginPackagePublisherRevocationProposalConflictError();
}
return Object.freeze({
status: 'existing' as const,
proposal,
});
}
await publisherSignerLock(
client,
proposal.actionInput.publisher,
proposal.actionInput.keyId,
);
const trust = await client.query<Row>(
`SELECT generation,
effective_trust_digest AS "effectiveTrustDigest"
FROM "ql3"."plugin_package_publisher_trust_heads"
WHERE authority_id = $1
LIMIT 2`,
[proposal.actionInput.trustAuthorityId],
);
if (
trust.rows.length !== 1 ||
postgresRequiredInteger(
trust.rows[0]!.generation,
unavailable,
) !== proposal.actionInput.trustGeneration ||
postgresRequiredString(
trust.rows[0]!.effectiveTrustDigest,
unavailable,
) !== proposal.actionInput.previousTrustDigest
) {
throw new PluginPackagePublisherRevocationProposalConflictError();
}
const fence = await client.query<Row>(
`SELECT "ql3"."lock_approval_policy_fence"(
$1::varchar, $2::varchar, $3::varchar, $4::integer, $5::integer
) AS "matches"`,
[
proposal.projectId,
proposal.proposedBy.type,
proposal.proposedBy.id,
proposal.proposalFence.projectVersion,
proposal.proposalFence.bindingVersion,
],
);
if (
fence.rows.length !== 1 ||
!postgresRequiredBoolean(fence.rows[0]!.matches, unavailable)
) {
throw new PluginPackagePublisherRevocationProposalConflictError();
}
const action = proposal.actionInput;
const inserted = await client.query(
`INSERT INTO
"ql3"."plugin_package_publisher_revocation_proposals" (
action_ref, project_id, authority_id, trust_generation,
publisher, key_id, previous_trust_digest,
current_trust_digest, action_type, permission, action_digest,
preview_digest, authorization_mode, reason_code,
proposed_by_type, proposed_by_id, proposer_assurance,
fence_project_version, fence_binding_version, created_at_ms,
proposal_json, proposal_digest
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
$14, $15, $16, $17, $18, $19, $20, $21::jsonb, $22
)`,
[
proposal.actionRef,
proposal.projectId,
action.trustAuthorityId,
action.trustGeneration,
action.publisher,
action.keyId,
action.previousTrustDigest,
action.currentTrustDigest,
proposal.actionType,
proposal.permission,
proposal.actionDigest,
proposal.previewDigest,
action.authorizationMode,
action.reasonCode,
proposal.proposedBy.type,
proposal.proposedBy.id,
proposal.proposerAssurance,
proposal.proposalFence.projectVersion,
proposal.proposalFence.bindingVersion,
proposal.createdAtMs,
JSON.stringify(proposal),
proposal.proposalDigest,
],
);
if (inserted.rowCount !== 1) throw unavailable();
const auditInserted = await client.query(
`INSERT INTO "ql3"."security_audit_events" (
event_id, request_id, operation_id, project_id, subject_type,
subject_id, authentication_id, outcome, reasons,
project_version, binding_version, occurred_at_ms
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11, $12
)`,
[
audit.eventId,
audit.requestId,
audit.operationId,
audit.projectId,
audit.subject?.type ?? null,
audit.subject?.id ?? null,
audit.authenticationId,
audit.outcome,
JSON.stringify(audit.reasons),
audit.fence?.projectVersion ?? null,
audit.fence?.bindingVersion ?? null,
audit.occurredAtMs,
],
);
if (auditInserted.rowCount !== 1) throw unavailable();
return Object.freeze({ status: 'created' as const, proposal });
});
}
}
@@ -0,0 +1,288 @@
// PostgreSQL Plugin Package publisher trust observation authority.
import type {
PostgresClient,
PostgresPool,
} from '@qinglong/runtime-core';
import {
PluginPackagePublisherTrustAuthorityConflictError,
PluginPackagePublisherTrustAuthorityUnavailableError,
createPluginPackagePublisherTrustHead,
normalizePluginPackagePublisherTrustHead,
normalizePluginPackagePublisherTrustSnapshot,
type ObservePluginPackagePublisherTrustSnapshotInput,
type ObservePluginPackagePublisherTrustSnapshotResult,
type PluginPackagePublisherTrustAuthorityRepository,
type PluginPackagePublisherTrustAuthorityState,
type PluginPackagePublisherTrustHead,
type PluginPackagePublisherTrustSnapshot,
} from '@qinglong/runtime-core/plugin-package-publisher-trust';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
type Queryable = Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>;
const AUTHORITY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
function unavailable(
cause?: unknown,
): PluginPackagePublisherTrustAuthorityUnavailableError {
return new PluginPackagePublisherTrustAuthorityUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function authorityId(value: unknown, label: string): string {
if (typeof value !== 'string' || !AUTHORITY_PATTERN.test(value)) {
throw new TypeError(`${label} is invalid`);
}
return value;
}
function timestamp(value: unknown): number {
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw new TypeError('publisher trust observation time is invalid');
}
return value as number;
}
function mappedError(error: unknown): Error {
if (
error instanceof PluginPackagePublisherTrustAuthorityConflictError ||
error instanceof PluginPackagePublisherTrustAuthorityUnavailableError ||
error instanceof TypeError
) {
return error;
}
const state = postgresSqlState(error);
if (state === '23503' || state === '23505' || state === '23514') {
return new PluginPackagePublisherTrustAuthorityConflictError();
}
return unavailable(error);
}
function parseHead(row: Row): Readonly<PluginPackagePublisherTrustHead> {
try {
const head = normalizePluginPackagePublisherTrustHead(
postgresRequiredJsonObject(
row.headJson,
unavailable,
) as unknown as PluginPackagePublisherTrustHead,
);
if (
head.headDigest !==
postgresRequiredString(row.headDigest, unavailable)
) {
throw unavailable();
}
return head;
} catch (error) {
if (error instanceof PluginPackagePublisherTrustAuthorityUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
function parseSnapshot(
row: Row,
): Readonly<PluginPackagePublisherTrustSnapshot> {
try {
const snapshot = normalizePluginPackagePublisherTrustSnapshot(
postgresRequiredJsonObject(
row.snapshotJson,
unavailable,
) as unknown as PluginPackagePublisherTrustSnapshot,
);
if (
snapshot.snapshotDigest !==
postgresRequiredString(row.snapshotDigest, unavailable)
) {
throw unavailable();
}
return snapshot;
} catch (error) {
if (error instanceof PluginPackagePublisherTrustAuthorityUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
async function authorityById(
queryable: Queryable,
value: string,
forUpdate = false,
): Promise<Readonly<PluginPackagePublisherTrustAuthorityState> | null> {
const result = await queryable.query<Row>(
`SELECT head.head_json AS "headJson",
head.head_digest AS "headDigest",
snapshot.snapshot_json AS "snapshotJson",
snapshot.snapshot_digest AS "snapshotDigest"
FROM "ql3"."plugin_package_publisher_trust_heads" AS head
JOIN "ql3"."plugin_package_publisher_trust_snapshots" AS snapshot
ON snapshot.snapshot_digest = head.effective_trust_digest
WHERE head.authority_id = $1
LIMIT 2${forUpdate ? ' FOR UPDATE OF head' : ''}`,
[value],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return Object.freeze({
head: parseHead(result.rows[0]!),
effectiveSnapshot: parseSnapshot(result.rows[0]!),
});
}
export class PostgresPluginPackagePublisherTrustAuthorityRepository
implements PluginPackagePublisherTrustAuthorityRepository
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError('PostgreSQL publisher trust pool is invalid');
}
}
async #transaction<T>(
work: (client: PostgresClient) => Promise<T>,
): Promise<T> {
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
throw mappedError(error);
}
let began = false;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const result = await work(client);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
state &&
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
) {
continue;
}
throw mappedError(error);
} finally {
client.release();
}
}
throw unavailable();
}
async findAuthority(
authorityIdValue: string,
): Promise<Readonly<PluginPackagePublisherTrustAuthorityState> | null> {
const id = authorityId(authorityIdValue, 'publisher trust authorityId');
try {
return await authorityById(this.pool, id);
} catch (error) {
throw mappedError(error);
}
}
observeSnapshot(
input: ObservePluginPackagePublisherTrustSnapshotInput,
): Promise<Readonly<ObservePluginPackagePublisherTrustSnapshotResult>> {
if (!input || typeof input !== 'object' || Array.isArray(input)) {
return Promise.reject(
new TypeError('publisher trust observation is invalid'),
);
}
const id = authorityId(input.authorityId, 'publisher trust authorityId');
const observedBy = authorityId(
input.observedBy,
'publisher trust observer',
);
const observedAtMs = timestamp(input.observedAtMs);
const snapshot = normalizePluginPackagePublisherTrustSnapshot(
input.snapshot,
);
if (snapshot.keys.length < 1) {
return Promise.reject(
new TypeError('publisher base trust snapshot must contain one key'),
);
}
const initialHead = createPluginPackagePublisherTrustHead(
id,
snapshot,
observedAtMs,
);
return this.#transaction(async (client) => {
await client.query(
`INSERT INTO "ql3"."plugin_package_publisher_trust_snapshots" (
snapshot_digest, key_count, observed_by, observed_at_ms,
snapshot_json
) VALUES ($1, $2, $3, $4, $5::jsonb)
ON CONFLICT (snapshot_digest) DO NOTHING`,
[
snapshot.snapshotDigest,
snapshot.keys.length,
observedBy,
observedAtMs,
JSON.stringify(snapshot),
],
);
const inserted = await client.query(
`INSERT INTO "ql3"."plugin_package_publisher_trust_heads" (
authority_id, generation, base_snapshot_digest,
effective_trust_digest, updated_at_ms, head_digest, head_json
) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)
ON CONFLICT (authority_id) DO NOTHING`,
[
initialHead.authorityId,
initialHead.generation,
initialHead.baseSnapshotDigest,
initialHead.effectiveTrustDigest,
initialHead.updatedAtMs,
initialHead.headDigest,
JSON.stringify(initialHead),
],
);
if (inserted.rowCount === 1) {
return Object.freeze({
status: 'created' as const,
head: initialHead,
effectiveSnapshot: snapshot,
});
}
if (inserted.rowCount !== 0) throw unavailable();
const existing = await authorityById(client, id);
if (!existing) {
throw new PluginPackagePublisherTrustAuthorityConflictError();
}
return Object.freeze({
status:
existing.head.baseSnapshotDigest === snapshot.snapshotDigest
? ('existing' as const)
: ('candidate' as const),
...existing,
});
});
}
}
@@ -0,0 +1,594 @@
// PostgreSQL Plugin Package publisher trust-transition proposal authority.
import type {
PostgresClient,
PostgresPool,
} from '@qinglong/runtime-core';
import {
approvalRequestDigest,
normalizeApprovalRequestRecord,
type ApprovalRequestRecord,
} from '@qinglong/runtime-core/approved-action';
import {
createPluginPackagePublisherTrustOverlapAdditionSnapshot,
createPluginPackagePublisherTrustRetirementSnapshot,
normalizePluginPackagePublisherTrustSnapshot,
type PluginPackagePublisherTrustSnapshot,
} from '@qinglong/runtime-core/plugin-package-publisher-trust';
import {
PluginPackagePublisherTrustTransitionConflictError,
PluginPackagePublisherTrustTransitionUnavailableError,
normalizePluginPackagePublisherTrustTransitionProposal,
type CreatePluginPackagePublisherTrustTransitionProposalCommand,
type CreatePluginPackagePublisherTrustTransitionProposalResult,
type PluginPackagePublisherTrustTransitionProposal,
type PluginPackagePublisherTrustTransitionProposalRepository,
} from '@qinglong/runtime-core/plugin-package-publisher-trust-transition-proposal';
import {
normalizeSecurityAuditRecord,
type SecurityAuditRecord,
} from '@qinglong/runtime-core/security-audit';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredBoolean,
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
type Queryable = Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>;
// Must stay byte-for-byte compatible with provenance and revocation mutations.
const SIGNER_ADVISORY_LOCK_SEED = 774635229;
async function publisherSignerLock(
queryable: Queryable,
publisher: string,
keyId: string,
): Promise<void> {
await queryable.query(
`SELECT pg_advisory_xact_lock(hashtextextended($1, $2))`,
[JSON.stringify([publisher, keyId]), SIGNER_ADVISORY_LOCK_SEED],
);
}
function unavailable(
cause?: unknown,
): PluginPackagePublisherTrustTransitionUnavailableError {
return new PluginPackagePublisherTrustTransitionUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function nullableString(value: unknown): string | null {
return value === null ? null : postgresRequiredString(value, unavailable);
}
function nullableInteger(value: unknown): number | null {
return value === null ? null : postgresRequiredInteger(value, unavailable);
}
function same(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function mappedError(error: unknown): Error {
if (
error instanceof PluginPackagePublisherTrustTransitionConflictError ||
error instanceof PluginPackagePublisherTrustTransitionUnavailableError
) {
return error;
}
const state = postgresSqlState(error);
if (state === '23503' || state === '23505' || state === '23514') {
return new PluginPackagePublisherTrustTransitionConflictError();
}
return unavailable(error);
}
function parseProposal(
row: Row,
): Readonly<PluginPackagePublisherTrustTransitionProposal> {
try {
const proposal = normalizePluginPackagePublisherTrustTransitionProposal(
postgresRequiredJsonObject(
row.proposalJson,
unavailable,
) as unknown as PluginPackagePublisherTrustTransitionProposal,
);
if (
proposal.proposalDigest !==
postgresRequiredString(row.proposalDigest, unavailable)
) {
throw unavailable();
}
return proposal;
} catch (error) {
if (error instanceof PluginPackagePublisherTrustTransitionUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
function parseSnapshot(
row: Row,
): Readonly<PluginPackagePublisherTrustSnapshot> {
try {
const snapshot = normalizePluginPackagePublisherTrustSnapshot(
postgresRequiredJsonObject(
row.snapshotJson,
unavailable,
) as unknown as PluginPackagePublisherTrustSnapshot,
);
if (
snapshot.snapshotDigest !==
postgresRequiredString(row.snapshotDigest, unavailable)
) {
throw unavailable();
}
return snapshot;
} catch (error) {
if (error instanceof PluginPackagePublisherTrustTransitionUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
function parseApprovalRequest(
row: Row,
): Readonly<ApprovalRequestRecord> {
try {
const request = normalizeApprovalRequestRecord(
postgresRequiredJsonObject(
row.requestJson,
unavailable,
) as unknown as ApprovalRequestRecord,
);
if (
approvalRequestDigest(request) !==
postgresRequiredString(row.requestDigest, unavailable)
) {
throw unavailable();
}
return request;
} catch (error) {
if (error instanceof PluginPackagePublisherTrustTransitionUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
function parseAudit(row: Row): Readonly<SecurityAuditRecord> {
try {
const subjectType = nullableString(row.subjectType);
const subjectId = nullableString(row.subjectId);
const projectVersion = nullableInteger(row.projectVersion);
if (!Array.isArray(row.reasons)) throw unavailable();
return normalizeSecurityAuditRecord({
eventId: postgresRequiredString(row.eventId, unavailable),
requestId: postgresRequiredString(row.requestId, unavailable),
operationId: postgresRequiredString(row.operationId, unavailable),
projectId: nullableString(row.projectId),
subject:
subjectType === null || subjectId === null
? null
: { type: subjectType, id: subjectId },
authenticationId: nullableString(row.authenticationId),
outcome: postgresRequiredString(row.outcome, unavailable),
reasons: row.reasons,
fence:
projectVersion === null
? null
: {
projectVersion,
bindingVersion: nullableInteger(row.bindingVersion),
},
occurredAtMs: postgresRequiredInteger(row.occurredAtMs, unavailable),
} as SecurityAuditRecord);
} catch (error) {
if (error instanceof PluginPackagePublisherTrustTransitionUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
async function proposalByActionRef(
queryable: Queryable,
actionRef: string,
): Promise<Readonly<PluginPackagePublisherTrustTransitionProposal> | null> {
const result = await queryable.query<Row>(
`SELECT proposal_json AS "proposalJson",
proposal_digest AS "proposalDigest"
FROM "ql3"."plugin_package_publisher_trust_transition_proposals"
WHERE action_ref = $1
LIMIT 2`,
[actionRef],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return parseProposal(result.rows[0]!);
}
async function snapshotByDigest(
queryable: Queryable,
snapshotDigest: string,
): Promise<Readonly<PluginPackagePublisherTrustSnapshot> | null> {
const result = await queryable.query<Row>(
`SELECT snapshot_json AS "snapshotJson",
snapshot_digest AS "snapshotDigest"
FROM "ql3"."plugin_package_publisher_trust_snapshots"
WHERE snapshot_digest = $1
LIMIT 2`,
[snapshotDigest],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return parseSnapshot(result.rows[0]!);
}
async function auditById(
queryable: Queryable,
eventId: string,
): Promise<Readonly<SecurityAuditRecord> | null> {
const result = await queryable.query<Row>(
`SELECT event_id AS "eventId", request_id AS "requestId",
operation_id AS "operationId", project_id AS "projectId",
subject_type AS "subjectType", subject_id AS "subjectId",
authentication_id AS "authenticationId", outcome,
reasons, project_version AS "projectVersion",
binding_version AS "bindingVersion",
occurred_at_ms AS "occurredAtMs"
FROM "ql3"."security_audit_events"
WHERE event_id = $1
LIMIT 2`,
[eventId],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return parseAudit(result.rows[0]!);
}
export function findPostgresPluginPackagePublisherTrustTransitionProposal(
queryable: Queryable,
actionRef: string,
): Promise<Readonly<PluginPackagePublisherTrustTransitionProposal> | null> {
return proposalByActionRef(queryable, actionRef);
}
export class PostgresPluginPackagePublisherTrustTransitionProposalRepository
implements PluginPackagePublisherTrustTransitionProposalRepository
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError(
'PostgreSQL publisher trust transition proposal pool is invalid',
);
}
}
async #transaction<T>(
work: (client: PostgresClient) => Promise<T>,
): Promise<T> {
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
throw mappedError(error);
}
let began = false;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const result = await work(client);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
state &&
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
) {
continue;
}
throw mappedError(error);
} finally {
client.release();
}
}
throw unavailable();
}
async findProposalByActionRef(
actionRef: string,
): Promise<Readonly<PluginPackagePublisherTrustTransitionProposal> | null> {
try {
return await proposalByActionRef(this.pool, actionRef);
} catch (error) {
throw mappedError(error);
}
}
async listApprovedRequests(
limitValue: number,
): Promise<readonly Readonly<ApprovalRequestRecord>[]> {
if (
!Number.isSafeInteger(limitValue) ||
limitValue < 1 ||
limitValue > 64
) {
throw new TypeError(
'publisher trust transition approval page limit is invalid',
);
}
try {
const result = await this.pool.query<Row>(
`SELECT request.request_json AS "requestJson",
request.request_digest AS "requestDigest"
FROM "ql3"."approval_requests" AS request
JOIN "ql3"."plugin_package_publisher_trust_transition_proposals"
AS proposal
ON proposal.action_ref = request.action_ref
WHERE request.state = 'approved'
AND request.request_json ->> 'decisionMode' =
'separation_of_duty'
AND request.action_type IN (
'plugin_package.publisher_key.overlap_add',
'plugin_package.publisher_key.safe_retire'
)
ORDER BY request.updated_at_ms, request.request_id
LIMIT $1`,
[limitValue],
);
if (result.rows.length > limitValue) throw unavailable();
return Object.freeze(result.rows.map(parseApprovalRequest));
} catch (error) {
throw mappedError(error);
}
}
createProposal(
command: CreatePluginPackagePublisherTrustTransitionProposalCommand,
): Promise<
Readonly<CreatePluginPackagePublisherTrustTransitionProposalResult>
> {
const proposal =
normalizePluginPackagePublisherTrustTransitionProposal(
command.proposal,
);
const candidateSnapshot =
normalizePluginPackagePublisherTrustSnapshot(
command.candidateSnapshot,
);
const audit = normalizeSecurityAuditRecord(command.audit);
if (
candidateSnapshot.snapshotDigest !==
proposal.actionInput.currentTrustDigest ||
audit.requestId !== proposal.actionRef ||
audit.operationId !==
'plugin_package.publisher_trust_transition.propose' ||
audit.projectId !== proposal.projectId ||
audit.subject?.type !== proposal.proposedBy.type ||
audit.subject.id !== proposal.proposedBy.id ||
audit.authenticationId === null ||
audit.outcome !== 'allowed' ||
!same(audit.reasons, ['publisher_trust_transition_proposal']) ||
audit.fence?.projectVersion !==
proposal.proposalFence.projectVersion ||
audit.fence.bindingVersion !==
proposal.proposalFence.bindingVersion ||
audit.occurredAtMs !== proposal.createdAtMs
) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
return this.#transaction(async (client) => {
const existing = await proposalByActionRef(
client,
proposal.actionRef,
);
if (existing) {
const [existingAudit, existingCandidate] = await Promise.all([
auditById(client, audit.eventId),
snapshotByDigest(client, candidateSnapshot.snapshotDigest),
]);
if (
!same(existing, proposal) ||
!existingAudit ||
!same(existingAudit, audit) ||
!existingCandidate ||
!same(existingCandidate, candidateSnapshot)
) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
return Object.freeze({
status: 'existing' as const,
proposal,
});
}
await publisherSignerLock(
client,
proposal.actionInput.publisher,
proposal.actionInput.keyId,
);
const trust = await client.query<Row>(
`SELECT head.generation,
head.effective_trust_digest AS "effectiveTrustDigest",
snapshot.snapshot_json AS "snapshotJson",
snapshot.snapshot_digest AS "snapshotDigest"
FROM "ql3"."plugin_package_publisher_trust_heads" AS head
JOIN "ql3"."plugin_package_publisher_trust_snapshots" AS snapshot
ON snapshot.snapshot_digest = head.effective_trust_digest
WHERE head.authority_id = $1
LIMIT 2`,
[proposal.actionInput.trustAuthorityId],
);
if (
trust.rows.length !== 1 ||
postgresRequiredInteger(
trust.rows[0]!.generation,
unavailable,
) !== proposal.actionInput.trustGeneration ||
postgresRequiredString(
trust.rows[0]!.effectiveTrustDigest,
unavailable,
) !== proposal.actionInput.previousTrustDigest
) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
const effectiveSnapshot = parseSnapshot(trust.rows[0]!);
let expectedCandidate: Readonly<PluginPackagePublisherTrustSnapshot>;
try {
expectedCandidate =
proposal.actionInput.mode === 'overlap_add'
? createPluginPackagePublisherTrustOverlapAdditionSnapshot(
effectiveSnapshot,
candidateSnapshot,
proposal.actionInput.publisher,
proposal.actionInput.keyId,
proposal.createdAtMs,
)
: createPluginPackagePublisherTrustRetirementSnapshot(
effectiveSnapshot,
proposal.actionInput.publisher,
proposal.actionInput.keyId,
proposal.createdAtMs,
);
} catch {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
if (!same(expectedCandidate, candidateSnapshot)) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
const fence = await client.query<Row>(
`SELECT "ql3"."lock_approval_policy_fence"(
$1::varchar, $2::varchar, $3::varchar, $4::integer, $5::integer
) AS "matches"`,
[
proposal.projectId,
proposal.proposedBy.type,
proposal.proposedBy.id,
proposal.proposalFence.projectVersion,
proposal.proposalFence.bindingVersion,
],
);
if (
fence.rows.length !== 1 ||
!postgresRequiredBoolean(fence.rows[0]!.matches, unavailable)
) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
const snapshotInsert = await client.query(
`INSERT INTO "ql3"."plugin_package_publisher_trust_snapshots" (
snapshot_digest, key_count, observed_by, observed_at_ms,
snapshot_json
) VALUES ($1, $2, $3, $4, $5::jsonb)
ON CONFLICT (snapshot_digest) DO NOTHING`,
[
candidateSnapshot.snapshotDigest,
candidateSnapshot.keys.length,
'cluster-package-manager',
proposal.createdAtMs,
JSON.stringify(candidateSnapshot),
],
);
if (
snapshotInsert.rowCount !== 0 &&
snapshotInsert.rowCount !== 1
) {
throw unavailable();
}
const storedCandidate = await snapshotByDigest(
client,
candidateSnapshot.snapshotDigest,
);
if (!storedCandidate || !same(storedCandidate, candidateSnapshot)) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
const action = proposal.actionInput;
const inserted = await client.query(
`INSERT INTO
"ql3"."plugin_package_publisher_trust_transition_proposals" (
action_ref, project_id, authority_id, trust_generation, mode,
publisher, key_id, previous_trust_digest,
current_trust_digest, action_type, permission, action_digest,
preview_digest, proposed_by_type, proposed_by_id,
proposer_assurance, fence_project_version,
fence_binding_version, created_at_ms, proposal_json,
proposal_digest
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
$14, $15, $16, $17, $18, $19, $20::jsonb, $21
)`,
[
proposal.actionRef,
proposal.projectId,
action.trustAuthorityId,
action.trustGeneration,
action.mode,
action.publisher,
action.keyId,
action.previousTrustDigest,
action.currentTrustDigest,
proposal.actionType,
proposal.permission,
proposal.actionDigest,
proposal.previewDigest,
proposal.proposedBy.type,
proposal.proposedBy.id,
proposal.proposerAssurance,
proposal.proposalFence.projectVersion,
proposal.proposalFence.bindingVersion,
proposal.createdAtMs,
JSON.stringify(proposal),
proposal.proposalDigest,
],
);
if (inserted.rowCount !== 1) throw unavailable();
const auditInserted = await client.query(
`INSERT INTO "ql3"."security_audit_events" (
event_id, request_id, operation_id, project_id, subject_type,
subject_id, authentication_id, outcome, reasons,
project_version, binding_version, occurred_at_ms
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11, $12
)`,
[
audit.eventId,
audit.requestId,
audit.operationId,
audit.projectId,
audit.subject?.type ?? null,
audit.subject?.id ?? null,
audit.authenticationId,
audit.outcome,
JSON.stringify(audit.reasons),
audit.fence?.projectVersion ?? null,
audit.fence?.bindingVersion ?? null,
audit.occurredAtMs,
],
);
if (auditInserted.rowCount !== 1) throw unavailable();
return Object.freeze({ status: 'created' as const, proposal });
});
}
}
@@ -0,0 +1,583 @@
// PostgreSQL Plugin Package publisher trust-transition execution authority.
import type {
PostgresClient,
PostgresPool,
} from '@qinglong/runtime-core';
import {
approvalRequestDigest,
approvedActionDispatchDigest,
normalizeApprovalRequestRecord,
normalizeApprovedActionDispatchRecord,
type ApprovalRequestRecord,
type ApprovedActionDispatchRecord,
} from '@qinglong/runtime-core/approved-action';
import {
advancePluginPackagePublisherTrustHead,
createPluginPackagePublisherTrustOverlapAdditionSnapshot,
createPluginPackagePublisherTrustRetirementSnapshot,
normalizePluginPackagePublisherTrustHead,
normalizePluginPackagePublisherTrustSnapshot,
type PluginPackagePublisherTrustHead,
type PluginPackagePublisherTrustSnapshot,
} from '@qinglong/runtime-core/plugin-package-publisher-trust';
import {
PluginPackagePublisherTrustTransitionConflictError,
PluginPackagePublisherTrustTransitionUnavailableError,
normalizePluginPackagePublisherTrustTransitionProposal,
normalizePluginPackagePublisherTrustTransitionReceipt,
resolvePluginPackagePublisherTrustTransitionProposal,
type PluginPackagePublisherTrustTransitionProposal,
type PluginPackagePublisherTrustTransitionReceipt,
} from '@qinglong/runtime-core/plugin-package-publisher-trust-transition-proposal';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
type Queryable = Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>;
const SIGNER_ADVISORY_LOCK_SEED = 774635229;
export interface ApplyPostgresPluginPackagePublisherTrustTransitionInput {
readonly dispatch: ApprovedActionDispatchRecord;
readonly executedAtMs: number;
}
export interface ApplyPostgresPluginPackagePublisherTrustTransitionResult {
readonly status: 'created' | 'existing';
readonly receipt: Readonly<PluginPackagePublisherTrustTransitionReceipt>;
readonly head: Readonly<PluginPackagePublisherTrustHead>;
}
function unavailable(
cause?: unknown,
): PluginPackagePublisherTrustTransitionUnavailableError {
return new PluginPackagePublisherTrustTransitionUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function mappedError(error: unknown): Error {
if (
error instanceof PluginPackagePublisherTrustTransitionConflictError ||
error instanceof PluginPackagePublisherTrustTransitionUnavailableError ||
error instanceof TypeError
) {
return error;
}
const state = postgresSqlState(error);
if (state === '23503' || state === '23505' || state === '23514') {
return new PluginPackagePublisherTrustTransitionConflictError();
}
return unavailable(error);
}
function same(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function timestamp(value: unknown): number {
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw new TypeError('publisher trust transition execution time is invalid');
}
return value as number;
}
function parseProposal(
row: Row,
): Readonly<PluginPackagePublisherTrustTransitionProposal> {
try {
const proposal = normalizePluginPackagePublisherTrustTransitionProposal(
postgresRequiredJsonObject(
row.proposalJson,
unavailable,
) as unknown as PluginPackagePublisherTrustTransitionProposal,
);
if (
proposal.proposalDigest !==
postgresRequiredString(row.proposalDigest, unavailable)
) {
throw unavailable();
}
return proposal;
} catch (error) {
if (error instanceof PluginPackagePublisherTrustTransitionUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
function parseDispatch(
row: Row,
): Readonly<ApprovedActionDispatchRecord> {
try {
const dispatch = normalizeApprovedActionDispatchRecord(
postgresRequiredJsonObject(
row.dispatchJson,
unavailable,
) as unknown as ApprovedActionDispatchRecord,
);
if (
approvedActionDispatchDigest(dispatch) !==
postgresRequiredString(row.dispatchDigest, unavailable)
) {
throw unavailable();
}
return dispatch;
} catch (error) {
if (error instanceof PluginPackagePublisherTrustTransitionUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
function parseApproval(
row: Row,
): Readonly<ApprovalRequestRecord> {
try {
const approval = normalizeApprovalRequestRecord(
postgresRequiredJsonObject(
row.approvalJson,
unavailable,
) as unknown as ApprovalRequestRecord,
);
if (
approvalRequestDigest(approval) !==
postgresRequiredString(row.approvalDigest, unavailable)
) {
throw unavailable();
}
return approval;
} catch (error) {
if (error instanceof PluginPackagePublisherTrustTransitionUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
function parseHead(row: Row): Readonly<PluginPackagePublisherTrustHead> {
try {
const head = normalizePluginPackagePublisherTrustHead(
postgresRequiredJsonObject(
row.headJson,
unavailable,
) as unknown as PluginPackagePublisherTrustHead,
);
if (
head.headDigest !== postgresRequiredString(row.headDigest, unavailable)
) {
throw unavailable();
}
return head;
} catch (error) {
if (error instanceof PluginPackagePublisherTrustTransitionUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
function parseSnapshot(
row: Row,
jsonKey: string,
digestKey: string,
): Readonly<PluginPackagePublisherTrustSnapshot> {
try {
const snapshot = normalizePluginPackagePublisherTrustSnapshot(
postgresRequiredJsonObject(
row[jsonKey],
unavailable,
) as unknown as PluginPackagePublisherTrustSnapshot,
);
if (
snapshot.snapshotDigest !==
postgresRequiredString(row[digestKey], unavailable)
) {
throw unavailable();
}
return snapshot;
} catch (error) {
if (error instanceof PluginPackagePublisherTrustTransitionUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
function parseReceipt(
row: Row,
): Readonly<PluginPackagePublisherTrustTransitionReceipt> {
try {
const receipt = normalizePluginPackagePublisherTrustTransitionReceipt(
postgresRequiredJsonObject(
row.receiptJson,
unavailable,
) as unknown as PluginPackagePublisherTrustTransitionReceipt,
);
if (
receipt.receiptDigest !==
postgresRequiredString(row.receiptDigest, unavailable)
) {
throw unavailable();
}
return receipt;
} catch (error) {
if (error instanceof PluginPackagePublisherTrustTransitionUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
async function publisherSignerLock(
queryable: Queryable,
publisher: string,
keyId: string,
): Promise<void> {
await queryable.query(
`SELECT pg_advisory_xact_lock(hashtextextended($1, $2))`,
[JSON.stringify([publisher, keyId]), SIGNER_ADVISORY_LOCK_SEED],
);
}
export class PostgresPluginPackagePublisherTrustTransitionRepository {
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError(
'PostgreSQL publisher trust transition pool is invalid',
);
}
}
async #transaction<T>(
work: (client: PostgresClient) => Promise<T>,
): Promise<T> {
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
throw mappedError(error);
}
let began = false;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const result = await work(client);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
state &&
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
) {
continue;
}
throw mappedError(error);
} finally {
client.release();
}
}
throw unavailable();
}
applyApprovedTransition(
input: ApplyPostgresPluginPackagePublisherTrustTransitionInput,
): Promise<
Readonly<ApplyPostgresPluginPackagePublisherTrustTransitionResult>
> {
if (
!input ||
typeof input !== 'object' ||
Array.isArray(input) ||
Object.keys(input).length !== 2 ||
!Object.hasOwn(input, 'dispatch') ||
!Object.hasOwn(input, 'executedAtMs')
) {
throw new TypeError('publisher trust transition execution is invalid');
}
const dispatch = normalizeApprovedActionDispatchRecord(input.dispatch);
const executedAtMs = timestamp(input.executedAtMs);
return this.#transaction(async (client) => {
const authority = await client.query<Row>(
`SELECT proposal.proposal_json AS "proposalJson",
proposal.proposal_digest AS "proposalDigest",
dispatch.dispatch_json AS "dispatchJson",
dispatch.dispatch_digest AS "dispatchDigest",
request.request_json AS "approvalJson",
request.request_digest AS "approvalDigest"
FROM "ql3"."approved_action_dispatches" AS dispatch
JOIN "ql3"."plugin_package_publisher_trust_transition_proposals"
AS proposal
ON proposal.action_ref = dispatch.action_ref
JOIN "ql3"."approval_requests" AS request
ON request.dispatch_id = dispatch.dispatch_id
WHERE dispatch.dispatch_id = $1
LIMIT 2`,
[dispatch.id],
);
if (authority.rows.length !== 1) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
const proposal = parseProposal(authority.rows[0]!);
const durableDispatch = parseDispatch(authority.rows[0]!);
const approval = parseApproval(authority.rows[0]!);
if (
!same(durableDispatch, dispatch) ||
approval.state !== 'consumed' ||
approval.version !== 3 ||
approval.decisionMode !== 'separation_of_duty' ||
approval.decision !== 'approved' ||
approval.id !== durableDispatch.approvalRequestId ||
approval.dispatchId !== durableDispatch.id ||
approval.projectId !== durableDispatch.projectId ||
!same(approval.action, durableDispatch.action) ||
!same(approval.requestedBy, durableDispatch.requestedBy) ||
!same(approval.consumedBy, durableDispatch.consumedBy) ||
!same(approval.decidedBy, durableDispatch.approvedBy) ||
approval.decisionAuthenticationId !==
durableDispatch.approvalAuthenticationId ||
approval.decisionAssurance !==
durableDispatch.approvalAssurance ||
approval.decidedAtMs !== durableDispatch.approvedAtMs ||
approval.expiresAtMs !== durableDispatch.expiresAtMs ||
!same(approval.decisionFence, durableDispatch.approvalFence) ||
approval.consumedAtMs !== durableDispatch.createdAtMs
) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
await publisherSignerLock(
client,
proposal.actionInput.publisher,
proposal.actionInput.keyId,
);
const existingReceipt = await client.query<Row>(
`SELECT receipt_json AS "receiptJson",
receipt_digest AS "receiptDigest"
FROM "ql3"."plugin_package_publisher_trust_transition_receipts"
WHERE mutation_id = $1 OR proposal_digest = $2
ORDER BY receipt_digest
LIMIT 2`,
[dispatch.id, proposal.proposalDigest],
);
if (existingReceipt.rows.length > 0) {
if (existingReceipt.rows.length !== 1) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
const receipt = parseReceipt(existingReceipt.rows[0]!);
const expected = resolvePluginPackagePublisherTrustTransitionProposal(
proposal,
dispatch,
executedAtMs,
proposal.actionInput.mode === 'safe_retire' ? 0 : null,
);
if (!same(receipt, expected)) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
const current = await client.query<Row>(
`SELECT head_json AS "headJson", head_digest AS "headDigest"
FROM "ql3"."plugin_package_publisher_trust_heads"
WHERE authority_id = $1
LIMIT 2`,
[receipt.trustAuthorityId],
);
if (current.rows.length !== 1) throw unavailable();
const head = parseHead(current.rows[0]!);
if (
head.generation !== receipt.currentGeneration ||
head.effectiveTrustDigest !== receipt.currentTrustDigest
) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
return Object.freeze({
status: 'existing' as const,
receipt,
head,
});
}
const trust = await client.query<Row>(
`SELECT head.head_json AS "headJson",
head.head_digest AS "headDigest",
effective.snapshot_json AS "effectiveSnapshotJson",
effective.snapshot_digest AS "effectiveSnapshotDigest",
candidate.snapshot_json AS "candidateSnapshotJson",
candidate.snapshot_digest AS "candidateSnapshotDigest"
FROM "ql3"."plugin_package_publisher_trust_heads" AS head
JOIN "ql3"."plugin_package_publisher_trust_snapshots" AS effective
ON effective.snapshot_digest = head.effective_trust_digest
JOIN "ql3"."plugin_package_publisher_trust_snapshots" AS candidate
ON candidate.snapshot_digest = $2
WHERE head.authority_id = $1
LIMIT 2
FOR UPDATE OF head`,
[
proposal.actionInput.trustAuthorityId,
proposal.actionInput.currentTrustDigest,
],
);
if (trust.rows.length !== 1) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
const head = parseHead(trust.rows[0]!);
const effectiveSnapshot = parseSnapshot(
trust.rows[0]!,
'effectiveSnapshotJson',
'effectiveSnapshotDigest',
);
const candidateSnapshot = parseSnapshot(
trust.rows[0]!,
'candidateSnapshotJson',
'candidateSnapshotDigest',
);
if (
head.generation !== proposal.actionInput.trustGeneration ||
head.effectiveTrustDigest !==
proposal.actionInput.previousTrustDigest ||
effectiveSnapshot.snapshotDigest !==
proposal.actionInput.previousTrustDigest ||
candidateSnapshot.snapshotDigest !==
proposal.actionInput.currentTrustDigest
) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
let expectedCandidate: Readonly<PluginPackagePublisherTrustSnapshot>;
try {
expectedCandidate =
proposal.actionInput.mode === 'overlap_add'
? createPluginPackagePublisherTrustOverlapAdditionSnapshot(
effectiveSnapshot,
candidateSnapshot,
proposal.actionInput.publisher,
proposal.actionInput.keyId,
executedAtMs,
)
: createPluginPackagePublisherTrustRetirementSnapshot(
effectiveSnapshot,
proposal.actionInput.publisher,
proposal.actionInput.keyId,
executedAtMs,
);
} catch {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
if (!same(expectedCandidate, candidateSnapshot)) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
let retirementMatchingInstallations: 0 | null = null;
if (proposal.actionInput.mode === 'safe_retire') {
const matching = await client.query<Row>(
`SELECT provenance.installation_id AS "installationId"
FROM "ql3"."plugin_package_publisher_provenance" AS provenance
JOIN "ql3"."plugin_package_install_heads" AS head
ON head.project_id = provenance.project_id
AND head.package_name = provenance.package_name
AND head.installation_id = provenance.installation_id
JOIN "ql3"."plugin_package_installs" AS install
ON install.installation_id = provenance.installation_id
WHERE provenance.publisher = $1 AND provenance.key_id = $2
AND install.state IN ('staged', 'activating', 'active')
LIMIT 1`,
[proposal.actionInput.publisher, proposal.actionInput.keyId],
);
if (matching.rows.length !== 0) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
retirementMatchingInstallations = 0;
}
const receipt = resolvePluginPackagePublisherTrustTransitionProposal(
proposal,
dispatch,
executedAtMs,
retirementMatchingInstallations,
);
const nextHead = advancePluginPackagePublisherTrustHead(
head,
candidateSnapshot,
executedAtMs,
);
const headUpdate = await client.query(
`UPDATE "ql3"."plugin_package_publisher_trust_heads"
SET generation = $2, effective_trust_digest = $3,
updated_at_ms = $4, head_digest = $5, head_json = $6::jsonb
WHERE authority_id = $1 AND generation = $7
AND effective_trust_digest = $8 AND head_digest = $9`,
[
nextHead.authorityId,
nextHead.generation,
nextHead.effectiveTrustDigest,
nextHead.updatedAtMs,
nextHead.headDigest,
JSON.stringify(nextHead),
head.generation,
head.effectiveTrustDigest,
head.headDigest,
],
);
if (headUpdate.rowCount !== 1) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
const receiptInsert = await client.query(
`INSERT INTO
"ql3"."plugin_package_publisher_trust_transition_receipts" (
mutation_id, proposal_digest, authority_id,
previous_generation, current_generation, mode, publisher,
key_id, previous_trust_digest, current_trust_digest,
proposer_type, proposer_id, confirmer_type, confirmer_id,
retirement_matching_installations, executed_at_ms,
receipt_json, receipt_digest
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
$14, $15, $16, $17::jsonb, $18
)`,
[
receipt.mutationId,
receipt.proposalDigest,
receipt.trustAuthorityId,
receipt.previousGeneration,
receipt.currentGeneration,
receipt.mode,
receipt.publisher,
receipt.keyId,
receipt.previousTrustDigest,
receipt.currentTrustDigest,
receipt.proposer.type,
receipt.proposer.id,
receipt.confirmer.type,
receipt.confirmer.id,
receipt.retirementMatchingInstallations,
receipt.executedAtMs,
JSON.stringify(receipt),
receipt.receiptDigest,
],
);
if (receiptInsert.rowCount !== 1) throw unavailable();
return Object.freeze({
status: 'created' as const,
receipt,
head: nextHead,
});
});
}
}
@@ -0,0 +1,840 @@
// PostgreSQL authorization authority for Plugin Package Workflow admission.
import {
RUN_CANCELLATION_REASONS,
RUN_STATUSES,
type PostgresClient,
type PostgresPool,
type RunCancellationReason,
type RunStatus,
} from '@qinglong/runtime-core';
import {
InvalidPluginPackageWorkflowAdministrationMutationError,
PLUGIN_PACKAGE_WORKFLOW_RUN_EVENT_LIST_SCHEMA,
PLUGIN_PACKAGE_WORKFLOW_RUN_INSPECTION_SCHEMA,
PLUGIN_PACKAGE_WORKFLOW_RUN_LIST_SCHEMA,
PLUGIN_PACKAGE_WORKFLOW_STEP_RUN_LIST_SCHEMA,
PluginPackageWorkflowAdministrationAuthorizationFenceConflictError,
PluginPackageWorkflowAdministrationMutationConflictError,
normalizeAuthorizedPluginPackageWorkflowRunEventList,
normalizeAuthorizedPluginPackageWorkflowRunInspection,
normalizeAuthorizedPluginPackageWorkflowRunList,
normalizeAuthorizedPluginPackageWorkflowStepRunList,
normalizeAuthorizedPluginPackageWorkflowAdmission,
normalizePluginPackageWorkflowRunEventListResult,
normalizePluginPackageWorkflowRunInspectionResult,
normalizePluginPackageWorkflowRunListResult,
normalizePluginPackageWorkflowStepRunListResult,
type AuthorizedPluginPackageWorkflowAdmission,
type AuthorizedPluginPackageWorkflowRunEventList,
type AuthorizedPluginPackageWorkflowRunInspection,
type AuthorizedPluginPackageWorkflowRunList,
type AuthorizedPluginPackageWorkflowStepRunList,
type PluginPackageWorkflowAdministrationRepository,
type PluginPackageWorkflowRunEventListRepository,
type PluginPackageWorkflowRunEventListResult,
type PluginPackageWorkflowRunInspectionRepository,
type PluginPackageWorkflowRunInspectionResult,
type PluginPackageWorkflowRunListRepository,
type PluginPackageWorkflowRunListResult,
type PluginPackageWorkflowStepRunListItem,
type PluginPackageWorkflowStepRunListRepository,
type PluginPackageWorkflowStepRunListResult,
} from '@qinglong/runtime-core/plugin-package-workflow-administration';
import type { PluginPackageWorkflowExecutionPlan } from '@qinglong/runtime-core/plugin-package-workflow-execution-plan';
import {
STEP_RUN_STATUSES,
type StepRunStatus,
} from '@qinglong/runtime-core/step-run';
import { PostgresPluginPackageWorkflowAdmissionRepository } from './pluginPackageWorkflowAdmissionRepository';
import {
configureAdministrationTransaction,
insertAdministrationAudit,
requiredInteger,
requiredString,
rollbackAdministrationTransaction,
} from '../../repository/administrationSupport';
type Row = Record<string, unknown>;
const API_CREDENTIAL_AUTHENTICATION =
/^api_credential:([A-Za-z0-9][A-Za-z0-9._:-]{0,63}):([1-9]\d*)$/;
interface WorkflowAuthorizationContext {
readonly projectId: string;
readonly actor: AuthorizedPluginPackageWorkflowAdmission['actor'];
readonly fence: AuthorizedPluginPackageWorkflowAdmission['fence'];
readonly audit: AuthorizedPluginPackageWorkflowAdmission['audit'];
}
function integer(value: unknown): number | null {
if (typeof value === 'number' && Number.isSafeInteger(value)) return value;
if (typeof value === 'string' && /^(0|[1-9]\d*)$/.test(value)) {
const parsed = Number(value);
if (Number.isSafeInteger(parsed)) return parsed;
}
return null;
}
function nullableInteger(row: Row, name: string): number | null {
if (row[name] === null) return null;
return requiredInteger(row, name);
}
function nullableString(row: Row, name: string): string | null {
if (row[name] === null) return null;
return requiredString(row, name);
}
function mutationConflict(): never {
throw new PluginPackageWorkflowAdministrationMutationConflictError();
}
function fenceConflict(): never {
throw new PluginPackageWorkflowAdministrationAuthorizationFenceConflictError();
}
async function confirmCredential(
client: PostgresClient,
authorization: Readonly<WorkflowAuthorizationContext>,
): Promise<void> {
const match = API_CREDENTIAL_AUTHENTICATION.exec(
authorization.audit.authenticationId ?? '',
);
const credentialVersion = integer(match?.[2]);
if (!match || credentialVersion === null || credentialVersion < 1) {
return fenceConflict();
}
await client.query('SELECT pg_advisory_xact_lock(hashtextextended($1, 0))', [
`ql3-api-credential:${match[1]}`,
]);
await client.query('SELECT pg_advisory_xact_lock(hashtextextended($1, 0))', [
`ql3-identity:${authorization.actor.type}:${authorization.actor.id}`,
]);
const result = await client.query<Row>(
`SELECT credential.version,
credential.state,
credential.subject_type AS "subjectType",
credential.subject_id AS "subjectId",
credential.not_before_at_ms AS "notBeforeAtMs",
credential.expires_at_ms AS "expiresAtMs",
subject.status AS "subjectStatus",
floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint
AS "nowMs"
FROM "ql3"."api_credentials" AS credential
JOIN "ql3"."identity_subjects" AS subject
ON subject.subject_type = credential.subject_type
AND subject.subject_id = credential.subject_id
WHERE credential.credential_id = $1
ORDER BY credential.version DESC
LIMIT 1`,
[match[1]],
);
const row = result.rows.length === 1 ? result.rows[0]! : null;
const nowMs = integer(row?.nowMs);
if (
!row ||
result.rows.length !== 1 ||
integer(row.version) !== credentialVersion ||
row.state !== 'active' ||
row.subjectStatus !== 'active' ||
row.subjectType !== authorization.actor.type ||
row.subjectId !== authorization.actor.id ||
nowMs === null ||
(integer(row.notBeforeAtMs) ?? Number.MAX_SAFE_INTEGER) > nowMs ||
(integer(row.expiresAtMs) ?? -1) <= nowMs
) {
return fenceConflict();
}
}
async function confirmProjectPolicyFence(
client: PostgresClient,
authorization: Readonly<WorkflowAuthorizationContext>,
): Promise<void> {
const project = await client.query<Row>(
`SELECT status, version
FROM "ql3"."projects"
WHERE id = $1
LIMIT 1
FOR SHARE`,
[authorization.projectId],
);
const projectRow = project.rows.length === 1 ? project.rows[0]! : null;
if (
!projectRow ||
projectRow.status !== 'active' ||
integer(projectRow.version) !== authorization.fence.projectVersion
) {
return fenceConflict();
}
const binding = await client.query<Row>(
`SELECT version, state
FROM "ql3"."project_role_bindings"
WHERE project_id = $1
AND subject_type = $2
AND subject_id = $3
ORDER BY version DESC
LIMIT 1`,
[authorization.projectId, authorization.actor.type, authorization.actor.id],
);
const bindingRow = binding.rows.length === 1 ? binding.rows[0]! : null;
if (
!bindingRow ||
bindingRow.state !== 'active' ||
integer(bindingRow.version) !== authorization.fence.bindingVersion
) {
return fenceConflict();
}
}
async function insertAtomicAudit(
client: PostgresClient,
admission: Readonly<AuthorizedPluginPackageWorkflowAdmission>,
replay: boolean,
): Promise<void> {
if (replay) return;
const audit = admission.audit;
await client.query(
`INSERT INTO "ql3"."security_audit_events" (
event_id, request_id, operation_id, project_id,
subject_type, subject_id, authentication_id, outcome, reasons,
project_version, binding_version, occurred_at_ms
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11, $12
)`,
[
audit.eventId,
audit.requestId,
audit.operationId,
audit.projectId,
audit.subject?.type ?? null,
audit.subject?.id ?? null,
audit.authenticationId,
audit.outcome,
JSON.stringify(audit.reasons),
audit.fence?.projectVersion ?? null,
audit.fence?.bindingVersion ?? null,
audit.occurredAtMs,
],
);
}
/**
* Adds API credential, Project Policy fence and mutation-audit checks to the
* PostgreSQL Workflow admission transaction used by cluster-control.
*/
export class PostgresAuthorizedPluginPackageWorkflowAdmissionRepository
implements PluginPackageWorkflowAdministrationRepository
{
private readonly admissions: PostgresPluginPackageWorkflowAdmissionRepository;
constructor(pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new InvalidPluginPackageWorkflowAdministrationMutationError(
'PostgreSQL pool is invalid',
);
}
this.admissions = new PostgresPluginPackageWorkflowAdmissionRepository(
pool,
);
}
findPlanByPlanId(
planId: string,
): Promise<Readonly<PluginPackageWorkflowExecutionPlan> | null> {
return this.admissions.findPlanByPlanId(planId);
}
async admitAuthorized(input: AuthorizedPluginPackageWorkflowAdmission) {
const admission = normalizeAuthorizedPluginPackageWorkflowAdmission(input);
const authorization = Object.freeze({
projectId: admission.plan.target.projectId,
actor: admission.actor,
fence: admission.fence,
audit: admission.audit,
});
return this.admissions.admit(admission.plan, async ({ client, replay }) => {
await confirmCredential(client, authorization);
await confirmProjectPolicyFence(client, authorization);
await insertAtomicAudit(client, admission, replay);
});
}
}
/**
* Reads the low-sensitive Package-bound Workflow Run projection from one
* serializable PostgreSQL snapshot after revalidating the credential and the
* latest Project Policy fence. It deliberately does not expose plan, task,
* attempt, executor, error, input/output or Secret material.
*/
export class PostgresAuthorizedPluginPackageWorkflowRunInspectionRepository
implements PluginPackageWorkflowRunInspectionRepository
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new InvalidPluginPackageWorkflowAdministrationMutationError(
'PostgreSQL pool is invalid',
);
}
}
async inspectRunAuthorized(
input: AuthorizedPluginPackageWorkflowRunInspection,
): Promise<Readonly<PluginPackageWorkflowRunInspectionResult>> {
const inspection =
normalizeAuthorizedPluginPackageWorkflowRunInspection(input);
let client: PostgresClient | undefined;
let began = false;
try {
client = await this.pool.connect();
await configureAdministrationTransaction(client);
began = true;
await confirmCredential(client, inspection);
await confirmProjectPolicyFence(client, inspection);
const targetRows = await client.query<Row>(
`SELECT admission.workflow_id AS "workflowId",
admission.step_count AS "stepCount",
run.status AS "runStatus",
run.version AS "runVersion",
run.event_sequence AS "eventSequence",
run.created_at_ms AS "createdAtMs",
run.queued_at_ms AS "queuedAtMs",
run.started_at_ms AS "startedAtMs",
run.finished_at_ms AS "finishedAtMs",
run.cancel_requested_at_ms AS "cancelRequestedAtMs",
run.cancel_reason AS "cancelReason"
FROM "ql3"."plugin_package_workflow_admissions" AS admission
JOIN "ql3"."runs" AS run
ON run.id = admission.run_id
AND run.project_id = admission.project_id
WHERE admission.run_id = $1
AND admission.project_id = $2
AND admission.package_name = $3
AND admission.workflow_id = $4
LIMIT 2`,
[
inspection.runId,
inspection.projectId,
inspection.packageName,
inspection.workflowId,
],
);
if (targetRows.rows.length === 0) {
await insertAdministrationAudit(client, inspection.audit);
const missing = normalizePluginPackageWorkflowRunInspectionResult({
schema: PLUGIN_PACKAGE_WORKFLOW_RUN_INSPECTION_SCHEMA,
found: false,
projectId: inspection.projectId,
packageName: inspection.packageName,
workflowId: inspection.workflowId,
runId: inspection.runId,
run: null,
stepCount: null,
stepStatusCounts: null,
});
await client.query('COMMIT');
began = false;
return missing;
}
if (targetRows.rows.length !== 1) mutationConflict();
const row = targetRows.rows[0]!;
if (requiredString(row, 'workflowId') !== inspection.workflowId) {
mutationConflict();
}
const stepCount = requiredInteger(row, 'stepCount');
if (stepCount < 1 || stepCount > 128) mutationConflict();
const statusRows = await client.query<Row>(
`SELECT status AS "stepStatus", COUNT(*) AS "statusCount"
FROM "ql3"."step_runs"
WHERE run_id = $1
GROUP BY status
ORDER BY status`,
[inspection.runId],
);
const stepStatusCounts = Object.fromEntries(
STEP_RUN_STATUSES.map((status) => [status, 0]),
) as Record<StepRunStatus, number>;
const observedStatuses = new Set<StepRunStatus>();
for (const statusRow of statusRows.rows) {
const status = requiredString(statusRow, 'stepStatus') as StepRunStatus;
if (
!STEP_RUN_STATUSES.includes(status) ||
observedStatuses.has(status)
) {
mutationConflict();
}
observedStatuses.add(status);
stepStatusCounts[status] = requiredInteger(statusRow, 'statusCount');
}
if (
Object.values(stepStatusCounts).reduce(
(total, count) => total + count,
0,
) !== stepCount
) {
mutationConflict();
}
const runStatus = requiredString(row, 'runStatus') as RunStatus;
const cancelReason = nullableString(
row,
'cancelReason',
) as RunCancellationReason | null;
if (
!RUN_STATUSES.includes(runStatus) ||
(cancelReason !== null &&
!RUN_CANCELLATION_REASONS.includes(cancelReason))
) {
mutationConflict();
}
const result = normalizePluginPackageWorkflowRunInspectionResult({
schema: PLUGIN_PACKAGE_WORKFLOW_RUN_INSPECTION_SCHEMA,
found: true,
projectId: inspection.projectId,
packageName: inspection.packageName,
workflowId: inspection.workflowId,
runId: inspection.runId,
run: {
status: runStatus,
version: requiredInteger(row, 'runVersion'),
eventSequence: requiredInteger(row, 'eventSequence'),
createdAtMs: requiredInteger(row, 'createdAtMs'),
queuedAtMs: nullableInteger(row, 'queuedAtMs'),
startedAtMs: nullableInteger(row, 'startedAtMs'),
finishedAtMs: nullableInteger(row, 'finishedAtMs'),
cancelRequestedAtMs: nullableInteger(row, 'cancelRequestedAtMs'),
cancelReason,
},
stepCount,
stepStatusCounts,
});
await insertAdministrationAudit(client, inspection.audit);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (client && began) await rollbackAdministrationTransaction(client);
if (
error instanceof
PluginPackageWorkflowAdministrationAuthorizationFenceConflictError ||
error instanceof
PluginPackageWorkflowAdministrationMutationConflictError ||
error instanceof InvalidPluginPackageWorkflowAdministrationMutationError
) {
throw error;
}
throw new PluginPackageWorkflowAdministrationMutationConflictError();
} finally {
client?.release();
}
}
}
/**
* Lists one newest-first, low-sensitive Workflow Run history page from a
* serializable authorization snapshot. The dedicated target/time index keeps
* the query bounded even when a Package owns many other Workflows.
*/
export class PostgresAuthorizedPluginPackageWorkflowRunListRepository
implements PluginPackageWorkflowRunListRepository
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new InvalidPluginPackageWorkflowAdministrationMutationError(
'PostgreSQL pool is invalid',
);
}
}
async listRunsAuthorized(
input: AuthorizedPluginPackageWorkflowRunList,
): Promise<Readonly<PluginPackageWorkflowRunListResult>> {
const query = normalizeAuthorizedPluginPackageWorkflowRunList(input);
let client: PostgresClient | undefined;
let began = false;
try {
client = await this.pool.connect();
await configureAdministrationTransaction(client);
began = true;
await confirmCredential(client, query);
await confirmProjectPolicyFence(client, query);
const page = await client.query<Row>(
`SELECT admission.run_id AS "runId",
admission.step_count AS "stepCount",
admission.admitted_at_ms AS "admittedAtMs",
run.status AS "runStatus",
run.version AS "runVersion",
run.event_sequence AS "eventSequence",
run.queued_at_ms AS "queuedAtMs",
run.started_at_ms AS "startedAtMs",
run.finished_at_ms AS "finishedAtMs",
run.cancel_requested_at_ms AS "cancelRequestedAtMs",
run.cancel_reason AS "cancelReason"
FROM "ql3"."plugin_package_workflow_admissions" AS admission
JOIN "ql3"."runs" AS run
ON run.id = admission.run_id
AND run.project_id = admission.project_id
WHERE admission.project_id = $1
AND admission.package_name = $2
AND admission.workflow_id = $3
AND ($4::bigint IS NULL OR admission.admitted_at_ms < $4 OR
(admission.admitted_at_ms = $4 AND admission.run_id < $5))
ORDER BY admission.admitted_at_ms DESC, admission.run_id DESC
LIMIT $6`,
[
query.projectId,
query.packageName,
query.workflowId,
query.after?.admittedAtMs ?? null,
query.after?.runId ?? null,
query.limit + 1,
],
);
const truncated = page.rows.length > query.limit;
const runs = page.rows.slice(0, query.limit).map((row) => ({
runId: requiredString(row, 'runId'),
status: requiredString(row, 'runStatus') as RunStatus,
version: requiredInteger(row, 'runVersion'),
eventSequence: requiredInteger(row, 'eventSequence'),
stepCount: requiredInteger(row, 'stepCount'),
admittedAtMs: requiredInteger(row, 'admittedAtMs'),
queuedAtMs: nullableInteger(row, 'queuedAtMs'),
startedAtMs: nullableInteger(row, 'startedAtMs'),
finishedAtMs: nullableInteger(row, 'finishedAtMs'),
cancelRequestedAtMs: nullableInteger(row, 'cancelRequestedAtMs'),
cancelReason: nullableString(
row,
'cancelReason',
) as RunCancellationReason | null,
}));
const last = runs.at(-1);
const result = normalizePluginPackageWorkflowRunListResult({
schema: PLUGIN_PACKAGE_WORKFLOW_RUN_LIST_SCHEMA,
projectId: query.projectId,
packageName: query.packageName,
workflowId: query.workflowId,
after: query.after,
runs,
truncated,
next:
truncated && last
? { admittedAtMs: last.admittedAtMs, runId: last.runId }
: null,
});
await insertAdministrationAudit(client, query.audit);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (client && began) await rollbackAdministrationTransaction(client);
if (
error instanceof
PluginPackageWorkflowAdministrationAuthorizationFenceConflictError ||
error instanceof
PluginPackageWorkflowAdministrationMutationConflictError ||
error instanceof InvalidPluginPackageWorkflowAdministrationMutationError
) {
throw error;
}
throw new PluginPackageWorkflowAdministrationMutationConflictError();
} finally {
client?.release();
}
}
}
/**
* Lists one bounded low-sensitive StepRun page behind the exact Package-bound
* Workflow target and current authorization fence. The runtime role only
* appends the allowed audit and never receives audit read authority.
*/
export class PostgresAuthorizedPluginPackageWorkflowStepRunListRepository
implements PluginPackageWorkflowStepRunListRepository
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new InvalidPluginPackageWorkflowAdministrationMutationError(
'PostgreSQL pool is invalid',
);
}
}
async listStepRunsAuthorized(
input: AuthorizedPluginPackageWorkflowStepRunList,
): Promise<Readonly<PluginPackageWorkflowStepRunListResult>> {
const query = normalizeAuthorizedPluginPackageWorkflowStepRunList(input);
let client: PostgresClient | undefined;
let began = false;
try {
client = await this.pool.connect();
await configureAdministrationTransaction(client);
began = true;
await confirmCredential(client, query);
await confirmProjectPolicyFence(client, query);
const targets = await client.query<Row>(
`SELECT admission.step_count AS "stepCount",
(SELECT COUNT(*) FROM "ql3"."step_runs" AS observed
WHERE observed.run_id = admission.run_id) AS "observedStepCount"
FROM "ql3"."plugin_package_workflow_admissions" AS admission
JOIN "ql3"."runs" AS run
ON run.id = admission.run_id
AND run.project_id = admission.project_id
WHERE admission.run_id = $1
AND admission.project_id = $2
AND admission.package_name = $3
AND admission.workflow_id = $4
LIMIT 2`,
[query.runId, query.projectId, query.packageName, query.workflowId],
);
if (targets.rows.length === 0) {
await insertAdministrationAudit(client, query.audit);
const missing = normalizePluginPackageWorkflowStepRunListResult({
schema: PLUGIN_PACKAGE_WORKFLOW_STEP_RUN_LIST_SCHEMA,
found: false,
projectId: query.projectId,
packageName: query.packageName,
workflowId: query.workflowId,
runId: query.runId,
stepRuns: [],
truncated: false,
next: null,
});
await client.query('COMMIT');
began = false;
return missing;
}
if (
targets.rows.length !== 1 ||
requiredInteger(targets.rows[0]!, 'stepCount') !==
requiredInteger(targets.rows[0]!, 'observedStepCount')
) {
mutationConflict();
}
const page = await client.query<Row>(
`SELECT id AS "id",
parent_step_run_id AS "parentStepRunId",
step_key AS "stepKey",
kind AS "kind",
required AS "required",
status AS "status",
version AS "version",
attempt_count AS "attemptCount",
ready_at_ms AS "readyAtMs",
started_at_ms AS "startedAtMs",
finished_at_ms AS "finishedAtMs",
result_code AS "resultCode",
created_at_ms AS "createdAtMs",
updated_at_ms AS "updatedAtMs"
FROM "ql3"."step_runs"
WHERE run_id = $1
AND ($2::varchar IS NULL OR step_key > $3 OR
(step_key = $3 AND id > $2))
ORDER BY step_key, id
LIMIT $4`,
[
query.runId,
query.after?.id ?? null,
query.after?.stepKey ?? '',
query.limit + 1,
],
);
const truncated = page.rows.length > query.limit;
const stepRuns = page.rows.slice(0, query.limit).map((row) => {
if (typeof row.required !== 'boolean') mutationConflict();
return {
id: requiredString(row, 'id'),
parentStepRunId: nullableString(row, 'parentStepRunId'),
stepKey: requiredString(row, 'stepKey'),
kind: requiredString(
row,
'kind',
) as PluginPackageWorkflowStepRunListItem['kind'],
required: row.required,
status: requiredString(row, 'status') as StepRunStatus,
version: requiredInteger(row, 'version'),
attemptCount: requiredInteger(row, 'attemptCount'),
readyAtMs: nullableInteger(row, 'readyAtMs'),
startedAtMs: nullableInteger(row, 'startedAtMs'),
finishedAtMs: nullableInteger(row, 'finishedAtMs'),
resultCode: nullableString(row, 'resultCode'),
createdAtMs: requiredInteger(row, 'createdAtMs'),
updatedAtMs: requiredInteger(row, 'updatedAtMs'),
};
});
const last = stepRuns.at(-1);
const result = normalizePluginPackageWorkflowStepRunListResult({
schema: PLUGIN_PACKAGE_WORKFLOW_STEP_RUN_LIST_SCHEMA,
found: true,
projectId: query.projectId,
packageName: query.packageName,
workflowId: query.workflowId,
runId: query.runId,
stepRuns,
truncated,
next: truncated && last ? { stepKey: last.stepKey, id: last.id } : null,
});
await insertAdministrationAudit(client, query.audit);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (client && began) await rollbackAdministrationTransaction(client);
if (
error instanceof
PluginPackageWorkflowAdministrationAuthorizationFenceConflictError ||
error instanceof
PluginPackageWorkflowAdministrationMutationConflictError ||
error instanceof InvalidPluginPackageWorkflowAdministrationMutationError
) {
throw error;
}
throw new PluginPackageWorkflowAdministrationMutationConflictError();
} finally {
client?.release();
}
}
}
/**
* Lists one bounded content-free RunEvent page behind the exact Package-bound
* Workflow target and current authorization fence.
*/
export class PostgresAuthorizedPluginPackageWorkflowRunEventListRepository
implements PluginPackageWorkflowRunEventListRepository
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new InvalidPluginPackageWorkflowAdministrationMutationError(
'PostgreSQL pool is invalid',
);
}
}
async listRunEventsAuthorized(
input: AuthorizedPluginPackageWorkflowRunEventList,
): Promise<Readonly<PluginPackageWorkflowRunEventListResult>> {
const query = normalizeAuthorizedPluginPackageWorkflowRunEventList(input);
let client: PostgresClient | undefined;
let began = false;
try {
client = await this.pool.connect();
await configureAdministrationTransaction(client);
began = true;
await confirmCredential(client, query);
await confirmProjectPolicyFence(client, query);
const targets = await client.query<Row>(
`SELECT run.event_sequence AS "headSequence"
FROM "ql3"."plugin_package_workflow_admissions" AS admission
JOIN "ql3"."runs" AS run
ON run.id = admission.run_id
AND run.project_id = admission.project_id
WHERE admission.run_id = $1
AND admission.project_id = $2
AND admission.package_name = $3
AND admission.workflow_id = $4
LIMIT 2`,
[query.runId, query.projectId, query.packageName, query.workflowId],
);
if (targets.rows.length === 0) {
await insertAdministrationAudit(client, query.audit);
const missing = normalizePluginPackageWorkflowRunEventListResult({
schema: PLUGIN_PACKAGE_WORKFLOW_RUN_EVENT_LIST_SCHEMA,
found: false,
projectId: query.projectId,
packageName: query.packageName,
workflowId: query.workflowId,
runId: query.runId,
afterSequence: query.afterSequence,
headSequence: null,
events: [],
truncated: false,
nextAfterSequence: null,
});
await client.query('COMMIT');
began = false;
return missing;
}
if (targets.rows.length !== 1) mutationConflict();
const headSequence = requiredInteger(targets.rows[0]!, 'headSequence');
const page = await client.query<Row>(
`SELECT id AS "id",
sequence AS "sequence",
type AS "type",
step_run_id AS "stepRunId",
created_at_ms AS "createdAtMs"
FROM "ql3"."run_events"
WHERE run_id = $1 AND sequence > $2
ORDER BY sequence, id
LIMIT $3`,
[query.runId, query.afterSequence, query.limit + 1],
);
const truncated = page.rows.length > query.limit;
const events = page.rows.slice(0, query.limit).map((row) => ({
id: requiredString(row, 'id'),
sequence: requiredInteger(row, 'sequence'),
type: requiredString(row, 'type'),
stepRunId: nullableString(row, 'stepRunId'),
createdAtMs: requiredInteger(row, 'createdAtMs'),
}));
const lastSequence = events.at(-1)?.sequence ?? null;
const result = normalizePluginPackageWorkflowRunEventListResult({
schema: PLUGIN_PACKAGE_WORKFLOW_RUN_EVENT_LIST_SCHEMA,
found: true,
projectId: query.projectId,
packageName: query.packageName,
workflowId: query.workflowId,
runId: query.runId,
afterSequence: query.afterSequence,
headSequence,
events,
truncated,
nextAfterSequence: truncated ? lastSequence : null,
});
await insertAdministrationAudit(client, query.audit);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (client && began) await rollbackAdministrationTransaction(client);
if (
error instanceof
PluginPackageWorkflowAdministrationAuthorizationFenceConflictError ||
error instanceof
PluginPackageWorkflowAdministrationMutationConflictError ||
error instanceof InvalidPluginPackageWorkflowAdministrationMutationError
) {
throw error;
}
throw new PluginPackageWorkflowAdministrationMutationConflictError();
} finally {
client?.release();
}
}
}
@@ -0,0 +1,843 @@
// PostgreSQL authority for atomic Plugin Package Workflow admission.
import type {
PostgresClient,
PostgresPool,
PostgresQueryable,
} from '@qinglong/runtime-core';
import {
InvalidPluginPackageWorkflowAdministrationMutationError,
PluginPackageWorkflowAdministrationAuthorizationFenceConflictError,
PluginPackageWorkflowAdministrationMutationConflictError,
} from '@qinglong/runtime-core/plugin-package-workflow-administration';
import {
normalizePluginPackageAutomationPublication,
type PluginPackageAutomationPublication,
} from '@qinglong/runtime-core/plugin-package-automation-publication';
import {
createPluginPackageWorkflowAdmissionBundle,
InvalidPluginPackageWorkflowExecutionPlanError,
normalizePluginPackageWorkflowAdmissionReceipt,
normalizePluginPackageWorkflowExecutionPlan,
pluginPackageWorkflowDefinitionDigest,
PluginPackageWorkflowAdmissionConflictError,
PluginPackageWorkflowAdmissionNotAllowedError,
PluginPackageWorkflowAdmissionUnavailableError,
type PluginPackageWorkflowAdmissionBundle,
type PluginPackageWorkflowAdmissionReceipt,
type PluginPackageWorkflowAdmissionRepository,
type PluginPackageWorkflowExecutionPlan,
} from '@qinglong/runtime-core/plugin-package-workflow-execution-plan';
import {
normalizeStepRunRecord,
type StepRunRecord,
} from '@qinglong/runtime-core/step-run';
import {
configurePostgresDefinitionTransaction,
postgresRequiredJsonObject,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
const IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const WORKFLOW_RUN_STATUSES = new Set([
'running',
'succeeded',
'failed',
'cancelled',
'timed_out',
]);
function unavailable(
cause?: unknown,
): PluginPackageWorkflowAdmissionUnavailableError {
return new PluginPackageWorkflowAdmissionUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function identity(value: unknown, label: string): string {
if (typeof value !== 'string' || !IDENTITY.test(value)) {
throw new InvalidPluginPackageWorkflowExecutionPlanError(
`${label} is invalid`,
);
}
return value;
}
function mapStorageError(error: unknown): Error {
if (
error instanceof InvalidPluginPackageWorkflowAdministrationMutationError ||
error instanceof
PluginPackageWorkflowAdministrationAuthorizationFenceConflictError ||
error instanceof PluginPackageWorkflowAdministrationMutationConflictError ||
error instanceof InvalidPluginPackageWorkflowExecutionPlanError ||
error instanceof PluginPackageWorkflowAdmissionConflictError ||
error instanceof PluginPackageWorkflowAdmissionNotAllowedError ||
error instanceof PluginPackageWorkflowAdmissionUnavailableError
) {
return error;
}
if (['23503', '23505', '23514'].includes(postgresSqlState(error) ?? '')) {
return new PluginPackageWorkflowAdmissionConflictError(
'durable Run, plan, StepRun, event, or receipt identity changed',
);
}
return unavailable(error);
}
export interface PostgresPluginPackageWorkflowAdmissionTransactionContext {
readonly client: PostgresClient;
readonly replay: boolean;
readonly plan: Readonly<PluginPackageWorkflowExecutionPlan>;
readonly receipt: Readonly<PluginPackageWorkflowAdmissionReceipt>;
}
export type PostgresPluginPackageWorkflowAdmissionTransactionGuard = (
context: Readonly<PostgresPluginPackageWorkflowAdmissionTransactionContext>,
) => void | Promise<void>;
function json(value: unknown): string {
if (
value === null ||
typeof value === 'boolean' ||
typeof value === 'number' ||
typeof value === 'string'
) {
return JSON.stringify(value);
}
if (Array.isArray(value)) {
return `[${value.map((entry) => json(entry ?? null)).join(',')}]`;
}
if (value && typeof value === 'object') {
const record = value as Readonly<Record<string, unknown>>;
return `{${Object.keys(record)
.filter((key) => record[key] !== undefined)
.sort()
.map((key) => `${JSON.stringify(key)}:${json(record[key])}`)
.join(',')}}`;
}
throw unavailable();
}
function text(row: Row, key: string): string {
const value = row[key];
if (typeof value !== 'string') throw unavailable();
return value;
}
function optionalText(row: Row, key: string): string | undefined {
const value = row[key];
if (value === null || value === undefined) return undefined;
if (typeof value !== 'string') throw unavailable();
return value;
}
function integer(row: Row, key: string): number {
const value = row[key];
if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) {
return value;
}
if (typeof value === 'string' && /^(0|[1-9]\d*)$/.test(value)) {
const parsed = Number(value);
if (Number.isSafeInteger(parsed)) return parsed;
}
throw unavailable();
}
function optionalInteger(row: Row, key: string): number | undefined {
if (row[key] === null || row[key] === undefined) return undefined;
return integer(row, key);
}
function exactArray(
left: readonly string[],
right: readonly string[],
): boolean {
return (
left.length === right.length &&
left.every((value, index) => value === right[index])
);
}
function parseStored(row: Row): Readonly<{
plan: Readonly<PluginPackageWorkflowExecutionPlan>;
receipt: Readonly<PluginPackageWorkflowAdmissionReceipt>;
bundle: Readonly<PluginPackageWorkflowAdmissionBundle>;
}> {
try {
const plan = normalizePluginPackageWorkflowExecutionPlan(
postgresRequiredJsonObject(
row.planJson,
unavailable,
) as unknown as PluginPackageWorkflowExecutionPlan,
);
const receipt = normalizePluginPackageWorkflowAdmissionReceipt(
postgresRequiredJsonObject(
row.receiptJson,
unavailable,
) as unknown as PluginPackageWorkflowAdmissionReceipt,
);
const bundle = createPluginPackageWorkflowAdmissionBundle(plan);
if (
plan.planDigest !== text(row, 'planDigest') ||
plan.planId !== text(row, 'planId') ||
plan.runId !== text(row, 'runId') ||
plan.target.projectId !== text(row, 'projectId') ||
plan.target.packageName !== text(row, 'packageName') ||
plan.target.installationId !== text(row, 'installationId') ||
plan.target.lockDigest !== text(row, 'lockDigest') ||
plan.target.generation !== integer(row, 'generation') ||
plan.target.generationDigest !== text(row, 'generationDigest') ||
plan.target.materializedRevisionDigest !==
text(row, 'materializedRevisionDigest') ||
plan.target.publicationDigest !== text(row, 'publicationDigest') ||
plan.target.workflowId !== text(row, 'workflowId') ||
plan.target.workflowDefinitionDigest !==
text(row, 'workflowDefinitionDigest') ||
plan.steps.length !== integer(row, 'stepCount') ||
receipt.admittedAtMs !== integer(row, 'admittedAtMs') ||
receipt.finalRunVersion !== integer(row, 'finalRunVersion') ||
receipt.finalRunEventSequence !== integer(row, 'finalRunEventSequence') ||
receipt.receiptDigest !== text(row, 'receiptDigest') ||
json(bundle.receipt) !== json(receipt)
) {
throw unavailable();
}
return Object.freeze({ plan, receipt, bundle });
} catch (error) {
if (error instanceof PluginPackageWorkflowAdmissionUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
async function assertStoredEvidence(
queryable: PostgresQueryable,
bundle: Readonly<PluginPackageWorkflowAdmissionBundle>,
): Promise<void> {
const run = bundle.run;
const storedRun = await queryable.query<Row>(
`SELECT project_id AS "projectId", task_id AS "taskId",
task_revision AS "taskRevision",
task_snapshot_ref AS "taskSnapshotRef",
trigger_type AS "triggerType",
execution_origin AS "executionOrigin",
execution_owner AS "executionOwner",
request_id AS "requestId", status, version,
event_sequence AS "eventSequence", priority,
idempotency_key AS "idempotencyKey",
created_at_ms AS "createdAtMs",
started_at_ms AS "startedAtMs"
FROM "ql3"."runs" WHERE id = $1`,
[run.id],
);
const runRow = storedRun.rows.length === 1 ? storedRun.rows[0]! : null;
const storedRunVersion = runRow ? integer(runRow, 'version') : -1;
const storedEventSequence = runRow ? integer(runRow, 'eventSequence') : -1;
if (
!runRow ||
text(runRow, 'projectId') !== run.projectId ||
text(runRow, 'taskId') !== run.taskId ||
text(runRow, 'taskRevision') !== run.taskRevision ||
optionalText(runRow, 'taskSnapshotRef') !== run.taskSnapshotRef ||
text(runRow, 'triggerType') !== run.triggerType ||
text(runRow, 'executionOrigin') !== run.executionOrigin ||
text(runRow, 'executionOwner') !== run.executionOwner ||
optionalText(runRow, 'requestId') !== run.requestId ||
!WORKFLOW_RUN_STATUSES.has(text(runRow, 'status')) ||
storedRunVersion < run.version ||
storedEventSequence < run.eventSequence ||
storedRunVersion !== storedEventSequence ||
integer(runRow, 'priority') !== run.priority ||
optionalText(runRow, 'idempotencyKey') !== run.idempotencyKey ||
integer(runRow, 'createdAtMs') !== run.createdAtMs ||
optionalInteger(runRow, 'startedAtMs') !== run.startedAtMs
) {
throw unavailable();
}
const storedEvents = await queryable.query<Row>(
`SELECT id, sequence, type, dedupe_key AS "dedupeKey",
actor_type AS "actorType", actor_id AS "actorId",
step_run_id AS "stepRunId", payload,
created_at_ms AS "createdAtMs"
FROM "ql3"."run_events"
WHERE run_id = $1 AND sequence <= $2
ORDER BY sequence`,
[run.id, bundle.receipt.finalRunEventSequence],
);
const expectedEvents = [
bundle.admissionEvent,
...bundle.stepMutations.map(({ event }) => event),
];
if (
storedEvents.rows.length !== expectedEvents.length ||
storedEvents.rows.some((row, index) => {
const event = expectedEvents[index]!;
return (
text(row, 'id') !== event.id ||
integer(row, 'sequence') !== event.sequence ||
text(row, 'type') !== event.type ||
optionalText(row, 'dedupeKey') !== event.dedupeKey ||
text(row, 'actorType') !== event.actorType ||
optionalText(row, 'actorId') !== event.actorId ||
optionalText(row, 'stepRunId') !== event.stepRunId ||
json(row.payload) !== json(event.payload) ||
integer(row, 'createdAtMs') !== event.createdAtMs
);
})
) {
throw unavailable();
}
for (const mutation of bundle.stepMutations) {
const stored = await queryable.query<Row>(
`SELECT
admission_step.step_run_id AS "stepRunId",
admission_step.task_id AS "taskId",
admission_step.task_definition_ref AS "taskDefinitionRef",
admission_step.task_definition_digest AS "taskDefinitionDigest",
admission_step.needs_json AS "needsJson",
admission_step.initial_status AS "initialStatus",
admission_step.mutation_id AS "mutationId",
admission_step.event_id AS "eventId",
runtime.step_key AS "currentStepKey",
runtime.kind AS "currentKind",
runtime.definition_ref AS "currentDefinitionRef",
runtime.definition_digest AS "currentDefinitionDigest",
runtime.required AS "currentRequired",
runtime.status AS "currentStatus",
runtime.version AS "currentVersion",
runtime.last_mutation_id AS "currentLastMutationId",
runtime.step_run_digest AS "currentStepRunDigest",
runtime.step_run_json AS "currentStepRunJson",
mutation.mutation_digest AS "mutationDigest",
mutation.event_sequence AS "eventSequence",
mutation.run_version AS "runVersion",
mutation.step_run_digest AS "initialStepRunDigest",
mutation.step_run_json AS "initialStepRunJson"
FROM "ql3"."plugin_package_workflow_admission_steps"
AS admission_step
JOIN "ql3"."step_runs" AS runtime
ON runtime.run_id = admission_step.run_id
AND runtime.id = admission_step.step_run_id
JOIN "ql3"."step_run_mutations" AS mutation
ON mutation.mutation_id = admission_step.mutation_id
WHERE admission_step.plan_digest = $1
AND admission_step.step_key = $2`,
[bundle.plan.planDigest, mutation.stepRun.stepKey],
);
const row = stored.rows.length === 1 ? stored.rows[0]! : null;
const planStep = bundle.plan.steps.find(
({ stepKey }) => stepKey === mutation.stepRun.stepKey,
);
let currentStepRun: Readonly<StepRunRecord> | null = null;
if (row) {
try {
currentStepRun = normalizeStepRunRecord(
postgresRequiredJsonObject(
row.currentStepRunJson,
unavailable,
) as unknown as StepRunRecord,
);
} catch {
throw unavailable();
}
}
if (
!row ||
!planStep ||
!currentStepRun ||
text(row, 'stepRunId') !== mutation.stepRun.id ||
text(row, 'taskId') !== planStep.taskId ||
text(row, 'taskDefinitionRef') !== planStep.taskDefinitionRef ||
text(row, 'taskDefinitionDigest') !== planStep.taskDefinitionDigest ||
json(row.needsJson) !== json(planStep.needs) ||
text(row, 'initialStatus') !== planStep.initialStatus ||
text(row, 'mutationId') !== mutation.mutationId ||
text(row, 'eventId') !== mutation.event.id ||
text(row, 'mutationDigest') !== mutation.mutationDigest ||
integer(row, 'eventSequence') !== mutation.event.sequence ||
integer(row, 'runVersion') !== mutation.expectedRunVersion + 1 ||
text(row, 'initialStepRunDigest') !== mutation.stepRun.stepRunDigest ||
json(row.initialStepRunJson) !== json(mutation.stepRun) ||
currentStepRun.id !== mutation.stepRun.id ||
currentStepRun.runId !== mutation.runId ||
currentStepRun.stepKey !== planStep.stepKey ||
currentStepRun.kind !== 'task' ||
currentStepRun.definitionRef !== planStep.taskDefinitionRef ||
currentStepRun.definitionDigest !== planStep.taskDefinitionDigest ||
currentStepRun.required !== planStep.required ||
currentStepRun.version < mutation.stepRun.version ||
text(row, 'currentStepKey') !== currentStepRun.stepKey ||
text(row, 'currentKind') !== currentStepRun.kind ||
text(row, 'currentDefinitionRef') !== currentStepRun.definitionRef ||
text(row, 'currentDefinitionDigest') !==
currentStepRun.definitionDigest ||
row.currentRequired !== currentStepRun.required ||
text(row, 'currentStatus') !== currentStepRun.status ||
integer(row, 'currentVersion') !== currentStepRun.version ||
text(row, 'currentLastMutationId') !== currentStepRun.lastMutationId ||
text(row, 'currentStepRunDigest') !== currentStepRun.stepRunDigest
) {
throw unavailable();
}
}
}
async function findStored(
queryable: PostgresQueryable,
column: 'plan_id' | 'run_id',
value: string,
): Promise<ReturnType<typeof parseStored> | null> {
const result = await queryable.query<Row>(
`SELECT
plan_digest AS "planDigest", plan_id AS "planId",
run_id AS "runId", project_id AS "projectId",
package_name AS "packageName",
installation_id AS "installationId",
lock_digest AS "lockDigest", generation,
generation_digest AS "generationDigest",
materialized_revision_digest AS "materializedRevisionDigest",
publication_digest AS "publicationDigest",
workflow_id AS "workflowId",
workflow_definition_digest AS "workflowDefinitionDigest",
step_count AS "stepCount", admitted_at_ms AS "admittedAtMs",
final_run_version AS "finalRunVersion",
final_run_event_sequence AS "finalRunEventSequence",
receipt_digest AS "receiptDigest",
plan_json AS "planJson", receipt_json AS "receiptJson"
FROM "ql3"."plugin_package_workflow_admissions"
WHERE ${column} = $1
LIMIT 2`,
[value],
);
if (result.rows.length > 1) throw unavailable();
if (!result.rows[0]) return null;
const stored = parseStored(result.rows[0]);
await assertStoredEvidence(queryable, stored.bundle);
return stored;
}
function assertSnapshot(
plan: Readonly<PluginPackageWorkflowExecutionPlan>,
row: Row,
): void {
let publication: Readonly<PluginPackageAutomationPublication>;
let resources: unknown;
try {
publication = normalizePluginPackageAutomationPublication(
postgresRequiredJsonObject(
row.publicationJson,
unavailable,
) as unknown as PluginPackageAutomationPublication,
);
resources = postgresRequiredJsonObject(
row.revisionJson,
unavailable,
).resources;
} catch (error) {
throw unavailable(error);
}
if (!Array.isArray(resources)) throw unavailable();
const target = plan.target;
const workflow = publication.definitions.workflows.find(
({ id }) => id === target.workflowId,
);
if (
publication.publicationDigest !== target.publicationDigest ||
publication.target.projectId !== target.projectId ||
publication.target.packageName !== target.packageName ||
publication.target.installationId !== target.installationId ||
publication.target.lockDigest !== target.lockDigest ||
publication.target.generation !== target.generation ||
publication.target.generationDigest !== target.generationDigest ||
publication.target.materializedRevisionDigest !==
target.materializedRevisionDigest ||
publication.state !== 'active' ||
!workflow ||
!workflow.enabled ||
pluginPackageWorkflowDefinitionDigest(workflow) !==
target.workflowDefinitionDigest ||
workflow.steps.length !== plan.steps.length
) {
throw new PluginPackageWorkflowAdmissionConflictError(
'the exact Workflow publication drifted',
);
}
for (const step of plan.steps) {
const workflowStep = workflow.steps.find(({ id }) => id === step.stepKey);
const matches = resources.filter((resource) => {
if (!resource || typeof resource !== 'object') return false;
const candidate = resource as {
kind?: unknown;
sourceDigest?: unknown;
value?: { id?: unknown; enabled?: unknown };
};
return (
candidate.kind === 'task' &&
candidate.sourceDigest === step.taskDefinitionDigest &&
candidate.value?.id === step.taskId &&
candidate.value.enabled === true
);
});
if (
!workflowStep ||
workflowStep.task !== step.taskId ||
!exactArray(workflowStep.needs, step.needs) ||
step.initialStatus !==
(workflowStep.needs.length === 0 ? 'ready' : 'pending') ||
step.taskDefinitionRef !==
`plugin-package:${target.materializedRevisionDigest}:task:${step.taskId}` ||
matches.length !== 1
) {
throw new PluginPackageWorkflowAdmissionConflictError(
'the exact Workflow step or Task evidence drifted',
);
}
}
}
async function insertRun(
client: PostgresClient,
bundle: Readonly<PluginPackageWorkflowAdmissionBundle>,
): Promise<void> {
const run = bundle.run;
await client.query(
`INSERT INTO "ql3"."runs" (
id, project_id, task_id, task_revision, task_snapshot_ref,
trigger_type, execution_origin, execution_owner, request_id,
status, version, event_sequence, priority, idempotency_key,
created_at_ms, started_at_ms
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14,
$15, $16
)`,
[
run.id,
run.projectId,
run.taskId,
run.taskRevision,
run.taskSnapshotRef ?? null,
run.triggerType,
run.executionOrigin,
run.executionOwner,
run.requestId ?? null,
run.status,
run.version,
run.eventSequence,
run.priority,
run.idempotencyKey ?? null,
run.createdAtMs,
run.startedAtMs ?? null,
],
);
}
async function insertEvent(
client: PostgresClient,
event: Readonly<PluginPackageWorkflowAdmissionBundle['admissionEvent']>,
stepRunId: string | null,
): Promise<void> {
await client.query(
`INSERT INTO "ql3"."run_events" (
id, run_id, sequence, type, dedupe_key, actor_type, actor_id,
attempt_id, step_run_id, payload, created_at_ms
) VALUES ($1, $2, $3, $4, $5, $6, $7, NULL, $8, $9::jsonb, $10)`,
[
event.id,
event.runId,
event.sequence,
event.type,
event.dedupeKey ?? null,
event.actorType,
event.actorId ?? null,
stepRunId,
json(event.payload),
event.createdAtMs,
],
);
}
async function insertStepEvidence(
client: PostgresClient,
bundle: Readonly<PluginPackageWorkflowAdmissionBundle>,
): Promise<void> {
for (const mutation of bundle.stepMutations) {
const step = mutation.stepRun;
await client.query(
`INSERT INTO "ql3"."step_runs" (
id, run_id, parent_step_run_id, step_key, kind, definition_ref,
definition_digest, required, status, version, attempt_count,
input_ref, output_ref, approval_request_id, ready_at_ms,
started_at_ms, finished_at_ms, result_code, error_summary,
created_at_ms, updated_at_ms, last_mutation_id, step_run_digest,
step_run_json
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14,
$15, $16, $17, $18, $19, $20, $21, $22, $23, $24::jsonb
)`,
[
step.id,
step.runId,
step.parentStepRunId,
step.stepKey,
step.kind,
step.definitionRef,
step.definitionDigest,
step.required,
step.status,
step.version,
step.attemptCount,
step.inputRef,
step.outputRef,
step.approvalRequestId,
step.readyAtMs,
step.startedAtMs,
step.finishedAtMs,
step.resultCode,
step.errorSummary,
step.createdAtMs,
step.updatedAtMs,
step.lastMutationId,
step.stepRunDigest,
json(step),
],
);
await insertEvent(client, mutation.event, step.id);
await client.query(
`INSERT INTO "ql3"."step_run_mutations" (
mutation_id, mutation_digest, run_id, step_run_id,
step_run_digest, event_id, event_sequence, run_version,
step_run_json, committed_at_ms
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10)`,
[
mutation.mutationId,
mutation.mutationDigest,
mutation.runId,
step.id,
step.stepRunDigest,
mutation.event.id,
mutation.event.sequence,
mutation.expectedRunVersion + 1,
json(step),
bundle.receipt.admittedAtMs,
],
);
}
}
async function insertAdmission(
client: PostgresClient,
bundle: Readonly<PluginPackageWorkflowAdmissionBundle>,
): Promise<void> {
const { plan, receipt } = bundle;
const target = plan.target;
await client.query(
`INSERT INTO "ql3"."plugin_package_workflow_admissions" (
plan_digest, plan_id, run_id, project_id, package_name,
installation_id, lock_digest, generation, generation_digest,
materialized_revision_digest, publication_digest, workflow_id,
workflow_definition_digest, step_count, admitted_at_ms,
final_run_version, final_run_event_sequence, receipt_digest,
plan_json, receipt_json
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14,
$15, $16, $17, $18, $19::jsonb, $20::jsonb
)`,
[
plan.planDigest,
plan.planId,
plan.runId,
target.projectId,
target.packageName,
target.installationId,
target.lockDigest,
target.generation,
target.generationDigest,
target.materializedRevisionDigest,
target.publicationDigest,
target.workflowId,
target.workflowDefinitionDigest,
plan.steps.length,
receipt.admittedAtMs,
receipt.finalRunVersion,
receipt.finalRunEventSequence,
receipt.receiptDigest,
json(plan),
json(receipt),
],
);
for (const step of plan.steps) {
const mutation = bundle.stepMutations.find(
({ stepRun }) => stepRun.stepKey === step.stepKey,
);
if (!mutation) throw unavailable();
await client.query(
`INSERT INTO "ql3"."plugin_package_workflow_admission_steps" (
plan_digest, run_id, step_key, step_run_id, task_id,
task_definition_ref, task_definition_digest, needs_json,
initial_status, mutation_id, event_id
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10, $11
)`,
[
plan.planDigest,
plan.runId,
step.stepKey,
step.stepRunId,
step.taskId,
step.taskDefinitionRef,
step.taskDefinitionDigest,
json(step.needs),
step.initialStatus,
mutation.mutationId,
mutation.event.id,
],
);
}
}
export class PostgresPluginPackageWorkflowAdmissionRepository
implements PluginPackageWorkflowAdmissionRepository
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw unavailable();
}
}
async findByPlanId(
planIdValue: string,
): Promise<Readonly<PluginPackageWorkflowAdmissionReceipt> | null> {
const planId = identity(planIdValue, 'planId');
try {
return (await findStored(this.pool, 'plan_id', planId))?.receipt ?? null;
} catch (error) {
throw mapStorageError(error);
}
}
async findByRunId(
runIdValue: string,
): Promise<Readonly<PluginPackageWorkflowAdmissionReceipt> | null> {
const runId = identity(runIdValue, 'runId');
try {
return (await findStored(this.pool, 'run_id', runId))?.receipt ?? null;
} catch (error) {
throw mapStorageError(error);
}
}
async findPlanByPlanId(
planIdValue: string,
): Promise<Readonly<PluginPackageWorkflowExecutionPlan> | null> {
const planId = identity(planIdValue, 'planId');
try {
return (await findStored(this.pool, 'plan_id', planId))?.plan ?? null;
} catch (error) {
throw mapStorageError(error);
}
}
async admit(
planValue: Readonly<PluginPackageWorkflowExecutionPlan>,
transactionGuard?: PostgresPluginPackageWorkflowAdmissionTransactionGuard,
): Promise<
Readonly<{
status: 'created' | 'existing';
receipt: Readonly<PluginPackageWorkflowAdmissionReceipt>;
}>
> {
if (
transactionGuard !== undefined &&
typeof transactionGuard !== 'function'
) {
throw new InvalidPluginPackageWorkflowExecutionPlanError(
'transaction guard is invalid',
);
}
const plan = normalizePluginPackageWorkflowExecutionPlan(planValue);
const bundle = createPluginPackageWorkflowAdmissionBundle(plan);
let client: PostgresClient | undefined;
let began = false;
try {
client = await this.pool.connect();
await configurePostgresDefinitionTransaction(client);
began = true;
const existing = await findStored(client, 'plan_id', plan.planId);
if (existing) {
if (
existing.plan.planDigest !== plan.planDigest ||
json(existing.plan) !== json(plan)
) {
throw new PluginPackageWorkflowAdmissionConflictError(
'planId is already bound to another plan',
);
}
await transactionGuard?.(
Object.freeze({
client,
replay: true,
plan: existing.plan,
receipt: existing.receipt,
}),
);
await client.query('COMMIT');
began = false;
return Object.freeze({
status: 'existing' as const,
receipt: existing.receipt,
});
}
await transactionGuard?.(
Object.freeze({
client,
replay: false,
plan,
receipt: bundle.receipt,
}),
);
const snapshot = await client.query<Row>(
`SELECT publication_json AS "publicationJson",
revision_json AS "revisionJson"
FROM "ql3"."plugin_package_workflow_admission_snapshot"(
$1, $2, $3
)`,
[
plan.target.projectId,
plan.target.packageName,
plan.target.publicationDigest,
],
);
if (snapshot.rows.length === 0) {
throw new PluginPackageWorkflowAdmissionNotAllowedError();
}
if (snapshot.rows.length !== 1) throw unavailable();
assertSnapshot(plan, snapshot.rows[0]!);
await insertRun(client, bundle);
await insertEvent(client, bundle.admissionEvent, null);
await insertStepEvidence(client, bundle);
await insertAdmission(client, bundle);
await client.query('COMMIT');
began = false;
return Object.freeze({
status: 'created' as const,
receipt: bundle.receipt,
});
} catch (error) {
if (client && began) await rollbackPostgresDefinitionTransaction(client);
throw mapStorageError(error);
} finally {
client?.release();
}
}
}
@@ -0,0 +1,736 @@
// PostgreSQL authority for advancing the Plugin Package Workflow frontier.
import type {
PostgresClient,
PostgresPool,
RunRecord,
} from '@qinglong/runtime-core';
import {
InvalidPluginPackageWorkflowFrontierError,
MAX_PLUGIN_PACKAGE_WORKFLOW_FRONTIER_PAGE_SIZE,
PluginPackageWorkflowFrontierConflictError,
PluginPackageWorkflowFrontierUnavailableError,
resolvePluginPackageWorkflowFrontier,
type PluginPackageWorkflowFrontierAdvanceResult,
type PluginPackageWorkflowFrontierCandidate,
type PluginPackageWorkflowFrontierCursor,
type PluginPackageWorkflowFrontierPage,
type PluginPackageWorkflowFrontierRepository,
type PluginPackageWorkflowTerminalStatus,
} from '@qinglong/runtime-core/plugin-package-workflow-frontier';
import {
normalizePluginPackageWorkflowExecutionPlan,
type PluginPackageWorkflowExecutionPlan,
} from '@qinglong/runtime-core/plugin-package-workflow-execution-plan';
import {
normalizeStepRunRecord,
type StepRunMutation,
type StepRunRecord,
} from '@qinglong/runtime-core/step-run';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredBoolean,
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
const IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const DIGEST = /^[0-9a-f]{64}$/;
const TERMINAL_RUN_STATUSES = new Set<PluginPackageWorkflowTerminalStatus>([
'succeeded',
'failed',
'cancelled',
'timed_out',
]);
const RUN_SELECT = `
id, project_id AS "projectId", task_id AS "taskId",
task_revision AS "taskRevision", task_name AS "taskName",
task_snapshot_ref AS "taskSnapshotRef", legacy_cron_id AS "legacyCronId",
parent_run_id AS "parentRunId", retry_of_run_id AS "retryOfRunId",
trigger_id AS "triggerId", trigger_type AS "triggerType",
execution_origin AS "executionOrigin",
execution_owner AS "executionOwner", triggered_by AS "triggeredBy",
request_id AS "requestId", scheduled_for_ms AS "scheduledForMs",
status, version, event_sequence AS "eventSequence", priority,
idempotency_key AS "idempotencyKey", input_ref AS "inputRef",
output_ref AS "outputRef", created_at_ms AS "createdAtMs",
queued_at_ms AS "queuedAtMs", started_at_ms AS "startedAtMs",
finished_at_ms AS "finishedAtMs",
cancel_requested_at_ms AS "cancelRequestedAtMs",
cancel_reason AS "cancelReason", error_code AS "errorCode",
error_summary AS "errorSummary"
`.trim();
const STEP_RUN_SELECT = `
id, run_id AS "runId", parent_step_run_id AS "parentStepRunId",
step_key AS "stepKey", kind, definition_ref AS "definitionRef",
definition_digest AS "definitionDigest", required, status, version,
attempt_count AS "attemptCount", input_ref AS "inputRef",
output_ref AS "outputRef", approval_request_id AS "approvalRequestId",
ready_at_ms AS "readyAtMs", started_at_ms AS "startedAtMs",
finished_at_ms AS "finishedAtMs", result_code AS "resultCode",
error_summary AS "errorSummary", created_at_ms AS "createdAtMs",
updated_at_ms AS "updatedAtMs", last_mutation_id AS "lastMutationId",
step_run_digest AS "stepRunDigest", step_run_json AS "stepRunJson"
`.trim();
function unavailable(
cause?: unknown,
): PluginPackageWorkflowFrontierUnavailableError {
return new PluginPackageWorkflowFrontierUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function text(row: Row, key: string): string {
return postgresRequiredString(row[key], unavailable);
}
function integer(row: Row, key: string): number {
const value = postgresRequiredInteger(row[key], unavailable);
if (value < 0) throw unavailable();
return value;
}
function optionalText(row: Row, key: string): string | undefined {
if (row[key] === null || row[key] === undefined) return undefined;
return text(row, key);
}
function optionalInteger(row: Row, key: string): number | undefined {
if (row[key] === null || row[key] === undefined) return undefined;
return integer(row, key);
}
function identity(value: unknown, label: string): string {
if (typeof value !== 'string' || !IDENTITY.test(value)) {
throw new InvalidPluginPackageWorkflowFrontierError(
`${label} is invalid`,
);
}
return value;
}
function digest(value: unknown): string {
if (typeof value !== 'string' || !DIGEST.test(value)) throw unavailable();
return value;
}
function mapStorageError(error: unknown): Error {
if (
error instanceof InvalidPluginPackageWorkflowFrontierError ||
error instanceof PluginPackageWorkflowFrontierConflictError ||
error instanceof PluginPackageWorkflowFrontierUnavailableError
) {
return error;
}
return ['23503', '23505', '23514'].includes(
postgresSqlState(error) ?? '',
)
? new PluginPackageWorkflowFrontierConflictError()
: unavailable(error);
}
function pageLimit(value: unknown): number {
if (
!Number.isInteger(value) ||
(value as number) < 1 ||
(value as number) > MAX_PLUGIN_PACKAGE_WORKFLOW_FRONTIER_PAGE_SIZE
) {
throw new InvalidPluginPackageWorkflowFrontierError(
`page limit must be between 1 and ${MAX_PLUGIN_PACKAGE_WORKFLOW_FRONTIER_PAGE_SIZE}`,
);
}
return value as number;
}
function cursor(
value: Readonly<PluginPackageWorkflowFrontierCursor> | undefined,
): Readonly<PluginPackageWorkflowFrontierCursor> | undefined {
if (value === undefined) return undefined;
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
Reflect.ownKeys(value).length !== 2 ||
!Reflect.has(value, 'admittedAtMs') ||
!Reflect.has(value, 'planDigest') ||
!Number.isSafeInteger(value.admittedAtMs) ||
value.admittedAtMs < 0 ||
!DIGEST.test(value.planDigest)
) {
throw new InvalidPluginPackageWorkflowFrontierError(
'frontier cursor is invalid',
);
}
return Object.freeze({
admittedAtMs: value.admittedAtMs,
planDigest: value.planDigest,
});
}
function runFromRow(row: Row): Readonly<RunRecord> {
const run: RunRecord = {
id: text(row, 'id'),
projectId: text(row, 'projectId'),
taskId: text(row, 'taskId'),
taskRevision: text(row, 'taskRevision'),
triggerType: text(row, 'triggerType'),
executionOrigin: text(
row,
'executionOrigin',
) as RunRecord['executionOrigin'],
executionOwner: text(row, 'executionOwner') as RunRecord['executionOwner'],
status: text(row, 'status') as RunRecord['status'],
version: integer(row, 'version'),
eventSequence: integer(row, 'eventSequence'),
priority: integer(row, 'priority'),
createdAtMs: integer(row, 'createdAtMs'),
};
const optionalTexts = [
['taskName', 'taskName'],
['taskSnapshotRef', 'taskSnapshotRef'],
['parentRunId', 'parentRunId'],
['retryOfRunId', 'retryOfRunId'],
['triggerId', 'triggerId'],
['triggeredBy', 'triggeredBy'],
['requestId', 'requestId'],
['idempotencyKey', 'idempotencyKey'],
['inputRef', 'inputRef'],
['outputRef', 'outputRef'],
['errorCode', 'errorCode'],
['errorSummary', 'errorSummary'],
] as const;
for (const [property, key] of optionalTexts) {
const value = optionalText(row, key);
if (value !== undefined) {
(run as unknown as Record<string, unknown>)[property] = value;
}
}
const optionalIntegers = [
['legacyCronId', 'legacyCronId'],
['scheduledForMs', 'scheduledForMs'],
['queuedAtMs', 'queuedAtMs'],
['startedAtMs', 'startedAtMs'],
['finishedAtMs', 'finishedAtMs'],
['cancelRequestedAtMs', 'cancelRequestedAtMs'],
] as const;
for (const [property, key] of optionalIntegers) {
const value = optionalInteger(row, key);
if (value !== undefined) {
(run as unknown as Record<string, unknown>)[property] = value;
}
}
const cancelReason = optionalText(row, 'cancelReason');
if (cancelReason !== undefined) {
run.cancelReason =
cancelReason as NonNullable<RunRecord['cancelReason']>;
}
return Object.freeze(run);
}
function stepRunFromRow(row: Row): Readonly<StepRunRecord> {
let stepRun: Readonly<StepRunRecord>;
try {
stepRun = normalizeStepRunRecord(
postgresRequiredJsonObject(
row.stepRunJson,
unavailable,
) as unknown as StepRunRecord,
);
} catch {
throw unavailable();
}
if (
text(row, 'id') !== stepRun.id ||
text(row, 'runId') !== stepRun.runId ||
optionalText(row, 'parentStepRunId') !==
(stepRun.parentStepRunId ?? undefined) ||
text(row, 'stepKey') !== stepRun.stepKey ||
text(row, 'kind') !== stepRun.kind ||
text(row, 'definitionRef') !== stepRun.definitionRef ||
text(row, 'definitionDigest') !== stepRun.definitionDigest ||
postgresRequiredBoolean(row.required, unavailable) !== stepRun.required ||
text(row, 'status') !== stepRun.status ||
integer(row, 'version') !== stepRun.version ||
integer(row, 'attemptCount') !== stepRun.attemptCount ||
optionalText(row, 'inputRef') !== (stepRun.inputRef ?? undefined) ||
optionalText(row, 'outputRef') !== (stepRun.outputRef ?? undefined) ||
optionalText(row, 'approvalRequestId') !==
(stepRun.approvalRequestId ?? undefined) ||
optionalInteger(row, 'readyAtMs') !==
(stepRun.readyAtMs ?? undefined) ||
optionalInteger(row, 'startedAtMs') !==
(stepRun.startedAtMs ?? undefined) ||
optionalInteger(row, 'finishedAtMs') !==
(stepRun.finishedAtMs ?? undefined) ||
optionalText(row, 'resultCode') !==
(stepRun.resultCode ?? undefined) ||
optionalText(row, 'errorSummary') !==
(stepRun.errorSummary ?? undefined) ||
integer(row, 'createdAtMs') !== stepRun.createdAtMs ||
integer(row, 'updatedAtMs') !== stepRun.updatedAtMs ||
text(row, 'lastMutationId') !== stepRun.lastMutationId ||
text(row, 'stepRunDigest') !== stepRun.stepRunDigest
) {
throw unavailable();
}
return stepRun;
}
function assertRunIdentity(
run: Readonly<RunRecord>,
plan: Readonly<PluginPackageWorkflowExecutionPlan>,
): void {
if (
run.id !== plan.runId ||
run.projectId !== plan.target.projectId ||
run.taskId !== plan.target.workflowId ||
run.taskRevision !== plan.target.publicationDigest ||
run.triggerType !== 'plugin_package_workflow' ||
run.executionOrigin !== 'system' ||
run.executionOwner !== 'runtime' ||
run.requestId !== plan.planId ||
run.idempotencyKey !== `plugin-package-workflow:${plan.planId}` ||
run.version !== run.eventSequence
) {
throw unavailable();
}
}
async function updateStepRun(
client: PostgresClient,
mutation: Readonly<StepRunMutation>,
): Promise<void> {
const stepRun = mutation.stepRun;
const updated = await client.query(
`UPDATE "ql3"."step_runs"
SET status = $1, version = $2, attempt_count = $3, output_ref = $4,
approval_request_id = $5, ready_at_ms = $6, started_at_ms = $7,
finished_at_ms = $8, result_code = $9, error_summary = $10,
updated_at_ms = $11, last_mutation_id = $12,
step_run_digest = $13, step_run_json = $14::jsonb
WHERE id = $15 AND run_id = $16 AND version = $17
AND step_run_digest = $18 AND status = $19`,
[
stepRun.status,
stepRun.version,
stepRun.attemptCount,
stepRun.outputRef,
stepRun.approvalRequestId,
stepRun.readyAtMs,
stepRun.startedAtMs,
stepRun.finishedAtMs,
stepRun.resultCode,
stepRun.errorSummary,
stepRun.updatedAtMs,
stepRun.lastMutationId,
stepRun.stepRunDigest,
JSON.stringify(stepRun),
stepRun.id,
stepRun.runId,
mutation.expectedStepRunVersion,
mutation.expectedStepRunDigest,
mutation.previousStatus,
],
);
if (updated.rowCount !== 1) {
throw new PluginPackageWorkflowFrontierConflictError();
}
}
async function insertStepMutation(
client: PostgresClient,
mutation: Readonly<StepRunMutation>,
committedAtMs: number,
): Promise<void> {
const event = mutation.event;
await client.query(
`INSERT INTO "ql3"."run_events" (
id, run_id, sequence, type, dedupe_key, actor_type, actor_id,
attempt_id, step_run_id, payload, created_at_ms
) VALUES ($1, $2, $3, $4, $5, $6, $7, NULL, $8, $9::jsonb, $10)`,
[
event.id,
event.runId,
event.sequence,
event.type,
event.dedupeKey ?? null,
event.actorType,
event.actorId ?? null,
mutation.stepRun.id,
JSON.stringify(event.payload),
event.createdAtMs,
],
);
await client.query(
`INSERT INTO "ql3"."step_run_mutations" (
mutation_id, mutation_digest, run_id, step_run_id, step_run_digest,
event_id, event_sequence, run_version, step_run_json, committed_at_ms
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10)`,
[
mutation.mutationId,
mutation.mutationDigest,
mutation.runId,
mutation.stepRun.id,
mutation.stepRun.stepRunDigest,
event.id,
event.sequence,
mutation.expectedRunVersion + 1,
JSON.stringify(mutation.stepRun),
committedAtMs,
],
);
}
export class PostgresPluginPackageWorkflowFrontierRepository
implements PluginPackageWorkflowFrontierRepository
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw unavailable();
}
}
async listCandidates(queryValue: Readonly<{
limit: number;
after?: Readonly<PluginPackageWorkflowFrontierCursor>;
}>): Promise<Readonly<PluginPackageWorkflowFrontierPage>> {
if (
!queryValue ||
typeof queryValue !== 'object' ||
Array.isArray(queryValue) ||
!Reflect.has(queryValue, 'limit') ||
Reflect.ownKeys(queryValue).some(
(key) => key !== 'limit' && key !== 'after',
)
) {
throw new InvalidPluginPackageWorkflowFrontierError(
'page query is invalid',
);
}
const limit = pageLimit(queryValue.limit);
const after = cursor(queryValue.after);
try {
const result = await this.pool.query<Row>(
`SELECT admission.run_id AS "runId",
admission.plan_digest AS "planDigest",
admission.admitted_at_ms AS "admittedAtMs"
FROM "ql3"."plugin_package_workflow_admissions" AS admission
JOIN "ql3"."runs" AS run ON run.id = admission.run_id
WHERE run.status = 'running'
AND run.cancel_requested_at_ms IS NULL
AND (
EXISTS (
SELECT 1
FROM "ql3"."plugin_package_workflow_admission_steps" AS step
JOIN "ql3"."step_runs" AS current
ON current.run_id = step.run_id
AND current.id = step.step_run_id
WHERE step.plan_digest = admission.plan_digest
AND current.status = 'pending'
AND (
NOT EXISTS (
SELECT 1
FROM jsonb_array_elements_text(step.needs_json)
AS need(value)
LEFT JOIN
"ql3"."plugin_package_workflow_admission_steps"
AS dependency_step
ON dependency_step.plan_digest = step.plan_digest
AND dependency_step.step_key = need.value
LEFT JOIN "ql3"."step_runs" AS dependency
ON dependency.run_id = dependency_step.run_id
AND dependency.id = dependency_step.step_run_id
WHERE dependency.id IS NULL
OR dependency.status <> 'succeeded'
)
OR EXISTS (
SELECT 1
FROM jsonb_array_elements_text(step.needs_json)
AS need(value)
JOIN "ql3"."plugin_package_workflow_admission_steps"
AS dependency_step
ON dependency_step.plan_digest = step.plan_digest
AND dependency_step.step_key = need.value
JOIN "ql3"."step_runs" AS dependency
ON dependency.run_id = dependency_step.run_id
AND dependency.id = dependency_step.step_run_id
WHERE dependency.status IN (
'failed', 'skipped', 'cancelled', 'timed_out'
)
)
)
)
OR NOT EXISTS (
SELECT 1
FROM "ql3"."plugin_package_workflow_admission_steps" AS step
JOIN "ql3"."step_runs" AS current
ON current.run_id = step.run_id
AND current.id = step.step_run_id
WHERE step.plan_digest = admission.plan_digest
AND current.status NOT IN (
'succeeded', 'failed', 'skipped', 'cancelled', 'timed_out'
)
)
)
AND (
$1::bigint IS NULL OR admission.admitted_at_ms > $1 OR
(admission.admitted_at_ms = $1
AND admission.plan_digest > $2)
)
ORDER BY admission.admitted_at_ms, admission.plan_digest
LIMIT $3`,
[
after?.admittedAtMs ?? null,
after?.planDigest ?? '',
limit + 1,
],
);
const mapped = result.rows.map(
(row): Readonly<PluginPackageWorkflowFrontierCandidate> =>
Object.freeze({
runId: identity(text(row, 'runId'), 'candidate runId'),
planDigest: digest(row.planDigest),
admittedAtMs: integer(row, 'admittedAtMs'),
}),
);
const truncated = mapped.length > limit;
const candidates = Object.freeze(mapped.slice(0, limit));
const last = candidates.at(-1);
return Object.freeze({
candidates,
truncated,
...(truncated && last
? {
next: Object.freeze({
admittedAtMs: last.admittedAtMs,
planDigest: last.planDigest,
}),
}
: {}),
});
} catch (error) {
throw mapStorageError(error);
}
}
async advance(
runIdValue: string,
): Promise<Readonly<PluginPackageWorkflowFrontierAdvanceResult>> {
const runId = identity(runIdValue, 'runId');
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
throw unavailable(error);
}
let began = false;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const admission = await client.query<Row>(
`SELECT plan_digest AS "planDigest", plan_json AS "planJson"
FROM "ql3"."plugin_package_workflow_admissions"
WHERE run_id = $1 LIMIT 2`,
[runId],
);
if (admission.rows.length !== 1) {
throw new PluginPackageWorkflowFrontierConflictError();
}
let plan: Readonly<PluginPackageWorkflowExecutionPlan>;
try {
plan = normalizePluginPackageWorkflowExecutionPlan(
postgresRequiredJsonObject(
admission.rows[0]!.planJson,
unavailable,
) as unknown as PluginPackageWorkflowExecutionPlan,
);
} catch {
throw unavailable();
}
if (plan.planDigest !== digest(admission.rows[0]!.planDigest)) {
throw unavailable();
}
const runRows = await client.query<Row>(
`SELECT ${RUN_SELECT}
FROM "ql3"."runs" WHERE id = $1 LIMIT 2 FOR UPDATE`,
[runId],
);
if (runRows.rows.length !== 1) {
throw new PluginPackageWorkflowFrontierConflictError();
}
const run = runFromRow(runRows.rows[0]!);
assertRunIdentity(run, plan);
const stepRows = await client.query<Row>(
`SELECT ${STEP_RUN_SELECT}
FROM "ql3"."step_runs" WHERE run_id = $1
ORDER BY step_key, id FOR UPDATE`,
[runId],
);
const stepRuns = stepRows.rows.map(stepRunFromRow);
const clock = await client.query<Row>(
`SELECT floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint AS "observedAtMs"`,
);
if (clock.rows.length !== 1) throw unavailable();
const observedAtMs = integer(clock.rows[0]!, 'observedAtMs');
const currentStatus = run.status;
const resolution = resolvePluginPackageWorkflowFrontier({
plan,
run: {
...run,
...(TERMINAL_RUN_STATUSES.has(
currentStatus as PluginPackageWorkflowTerminalStatus,
)
? { status: 'running' as const }
: {}),
},
stepRuns,
observedAtMs,
});
if (
TERMINAL_RUN_STATUSES.has(
currentStatus as PluginPackageWorkflowTerminalStatus,
)
) {
if (
resolution.stepMutations.length !== 0 ||
resolution.terminalStatus !== currentStatus
) {
throw unavailable();
}
await client.query('COMMIT');
began = false;
return Object.freeze({
status: 'settled' as const,
runId,
planDigest: plan.planDigest,
stepMutationCount: 0,
readyStepRunIds: Object.freeze([]),
terminalStatus:
currentStatus as PluginPackageWorkflowTerminalStatus,
runVersion: run.version,
runEventSequence: run.eventSequence,
observedAtMs,
});
}
if (currentStatus !== 'running') throw unavailable();
const increment =
resolution.stepMutations.length +
(resolution.terminalTransition === null ? 0 : 1);
if (increment === 0) {
await client.query('COMMIT');
began = false;
return Object.freeze({
status: 'unchanged' as const,
runId,
planDigest: plan.planDigest,
stepMutationCount: 0,
readyStepRunIds: resolution.readyStepRunIds,
terminalStatus: null,
runVersion: run.version,
runEventSequence: run.eventSequence,
observedAtMs,
});
}
for (const mutation of resolution.stepMutations) {
await updateStepRun(client, mutation);
}
const terminal = resolution.terminalTransition;
const updatedRun = await client.query(
`UPDATE "ql3"."runs"
SET status = $1, version = version + $2,
event_sequence = event_sequence + $2,
finished_at_ms = $3, error_code = $4, error_summary = NULL
WHERE id = $5 AND status = 'running'
AND version = $6 AND event_sequence = $7`,
[
terminal?.status ?? 'running',
increment,
terminal?.finishedAtMs ?? null,
terminal?.errorCode ?? null,
runId,
run.version,
run.eventSequence,
],
);
if (updatedRun.rowCount !== 1) {
throw new PluginPackageWorkflowFrontierConflictError();
}
for (const mutation of resolution.stepMutations) {
await insertStepMutation(client, mutation, observedAtMs);
}
if (terminal) {
const event = terminal.event;
await client.query(
`INSERT INTO "ql3"."run_events" (
id, run_id, sequence, type, dedupe_key, actor_type, actor_id,
attempt_id, step_run_id, payload, created_at_ms
) VALUES (
$1, $2, $3, $4, $5, $6, $7, NULL, NULL, $8::jsonb, $9
)`,
[
event.id,
event.runId,
event.sequence,
event.type,
event.dedupeKey ?? null,
event.actorType,
event.actorId ?? null,
JSON.stringify(event.payload),
event.createdAtMs,
],
);
}
await client.query('COMMIT');
began = false;
return Object.freeze({
status: terminal ? ('terminal' as const) : ('advanced' as const),
runId,
planDigest: plan.planDigest,
stepMutationCount: resolution.stepMutations.length,
readyStepRunIds: terminal
? Object.freeze([])
: resolution.readyStepRunIds,
terminalStatus: terminal?.status ?? null,
runVersion: run.version + increment,
runEventSequence: run.eventSequence + increment,
observedAtMs,
});
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
state &&
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
) {
continue;
}
throw mapStorageError(error);
} finally {
client.release();
}
}
throw unavailable();
}
}
@@ -0,0 +1,777 @@
// PostgreSQL authority for admitting Plugin Package Workflow task attempts.
import type {
PostgresClient,
PostgresPool,
RunRecord,
} from '@qinglong/runtime-core';
import {
normalizeClusterTaskExecutionRevision,
type ClusterTaskExecutionRevision,
} from '@qinglong/runtime-core/cluster-execution-revision';
import {
createPluginPackageWorkflowTaskAttemptAdmission,
InvalidPluginPackageWorkflowTaskAttemptAdmissionError,
MAX_PLUGIN_PACKAGE_WORKFLOW_TASK_ATTEMPT_PAGE_SIZE,
normalizePluginPackageWorkflowTaskAttemptAdmissionReceipt,
PluginPackageWorkflowTaskAttemptAdmissionConflictError,
PluginPackageWorkflowTaskAttemptAdmissionUnavailableError,
type PluginPackageWorkflowTaskAttemptAdmissionCandidate,
type PluginPackageWorkflowTaskAttemptAdmissionCursor,
type PluginPackageWorkflowTaskAttemptAdmissionPage,
type PluginPackageWorkflowTaskAttemptAdmissionReceipt,
type PluginPackageWorkflowTaskAttemptAdmissionRepository,
type PluginPackageWorkflowTaskAttemptAdmissionResult,
} from '@qinglong/runtime-core/plugin-package-workflow-task-attempt-admission';
import {
normalizePluginPackageTaskReconciliationReceipt,
type PluginPackageTaskReconciliationReceipt,
} from '@qinglong/runtime-core/plugin-package-task-reconciliation';
import {
normalizePluginPackageWorkflowExecutionPlan,
type PluginPackageWorkflowExecutionPlan,
} from '@qinglong/runtime-core/plugin-package-workflow-execution-plan';
import {
normalizeStepRunRecord,
type StepRunRecord,
} from '@qinglong/runtime-core/step-run';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredBoolean,
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
const IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const DIGEST = /^[0-9a-f]{64}$/;
const RUN_SELECT = `
id, project_id AS "projectId", task_id AS "taskId",
task_revision AS "taskRevision", task_name AS "taskName",
task_snapshot_ref AS "taskSnapshotRef", legacy_cron_id AS "legacyCronId",
parent_run_id AS "parentRunId", retry_of_run_id AS "retryOfRunId",
trigger_id AS "triggerId", trigger_type AS "triggerType",
execution_origin AS "executionOrigin",
execution_owner AS "executionOwner", triggered_by AS "triggeredBy",
request_id AS "requestId", scheduled_for_ms AS "scheduledForMs",
status, version, event_sequence AS "eventSequence", priority,
idempotency_key AS "idempotencyKey", input_ref AS "inputRef",
output_ref AS "outputRef", created_at_ms AS "createdAtMs",
queued_at_ms AS "queuedAtMs", started_at_ms AS "startedAtMs",
finished_at_ms AS "finishedAtMs",
cancel_requested_at_ms AS "cancelRequestedAtMs",
cancel_reason AS "cancelReason", error_code AS "errorCode",
error_summary AS "errorSummary"
`.trim();
const STEP_RUN_SELECT = `
id, run_id AS "runId", parent_step_run_id AS "parentStepRunId",
step_key AS "stepKey", kind, definition_ref AS "definitionRef",
definition_digest AS "definitionDigest", required, status, version,
attempt_count AS "attemptCount", input_ref AS "inputRef",
output_ref AS "outputRef", approval_request_id AS "approvalRequestId",
ready_at_ms AS "readyAtMs", started_at_ms AS "startedAtMs",
finished_at_ms AS "finishedAtMs", result_code AS "resultCode",
error_summary AS "errorSummary", created_at_ms AS "createdAtMs",
updated_at_ms AS "updatedAtMs", last_mutation_id AS "lastMutationId",
step_run_digest AS "stepRunDigest", step_run_json AS "stepRunJson"
`.trim();
function unavailable(
cause?: unknown,
): PluginPackageWorkflowTaskAttemptAdmissionUnavailableError {
return new PluginPackageWorkflowTaskAttemptAdmissionUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function text(row: Row, key: string): string {
return postgresRequiredString(row[key], unavailable);
}
function integer(row: Row, key: string): number {
const value = postgresRequiredInteger(row[key], unavailable);
if (value < 0) throw unavailable();
return value;
}
function optionalText(row: Row, key: string): string | undefined {
if (row[key] === null || row[key] === undefined) return undefined;
return text(row, key);
}
function optionalInteger(row: Row, key: string): number | undefined {
if (row[key] === null || row[key] === undefined) return undefined;
return integer(row, key);
}
function identity(value: unknown, label: string): string {
if (typeof value !== 'string' || !IDENTITY.test(value)) {
throw new InvalidPluginPackageWorkflowTaskAttemptAdmissionError(
`${label} is invalid`,
);
}
return value;
}
function digest(value: unknown): string {
if (typeof value !== 'string' || !DIGEST.test(value)) throw unavailable();
return value;
}
function mapStorageError(error: unknown): Error {
if (
error instanceof
InvalidPluginPackageWorkflowTaskAttemptAdmissionError ||
error instanceof
PluginPackageWorkflowTaskAttemptAdmissionConflictError ||
error instanceof
PluginPackageWorkflowTaskAttemptAdmissionUnavailableError
) {
return error;
}
return ['23503', '23505', '23514'].includes(
postgresSqlState(error) ?? '',
)
? new PluginPackageWorkflowTaskAttemptAdmissionConflictError()
: unavailable(error);
}
function pageLimit(value: unknown): number {
if (
!Number.isInteger(value) ||
(value as number) < 1 ||
(value as number) >
MAX_PLUGIN_PACKAGE_WORKFLOW_TASK_ATTEMPT_PAGE_SIZE
) {
throw new InvalidPluginPackageWorkflowTaskAttemptAdmissionError(
`page limit must be between 1 and ${MAX_PLUGIN_PACKAGE_WORKFLOW_TASK_ATTEMPT_PAGE_SIZE}`,
);
}
return value as number;
}
function cursor(
value:
| Readonly<PluginPackageWorkflowTaskAttemptAdmissionCursor>
| undefined,
):
| Readonly<PluginPackageWorkflowTaskAttemptAdmissionCursor>
| undefined {
if (value === undefined) return undefined;
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
Reflect.ownKeys(value).length !== 2 ||
!Reflect.has(value, 'readyAtMs') ||
!Reflect.has(value, 'stepRunId') ||
!Number.isSafeInteger(value.readyAtMs) ||
value.readyAtMs < 0 ||
typeof value.stepRunId !== 'string' ||
!IDENTITY.test(value.stepRunId)
) {
throw new InvalidPluginPackageWorkflowTaskAttemptAdmissionError(
'candidate cursor is invalid',
);
}
return Object.freeze({
readyAtMs: value.readyAtMs,
stepRunId: value.stepRunId,
});
}
function runFromRow(row: Row): Readonly<RunRecord> {
const run: RunRecord = {
id: text(row, 'id'),
projectId: text(row, 'projectId'),
taskId: text(row, 'taskId'),
taskRevision: text(row, 'taskRevision'),
triggerType: text(row, 'triggerType'),
executionOrigin: text(
row,
'executionOrigin',
) as RunRecord['executionOrigin'],
executionOwner: text(row, 'executionOwner') as RunRecord['executionOwner'],
status: text(row, 'status') as RunRecord['status'],
version: integer(row, 'version'),
eventSequence: integer(row, 'eventSequence'),
priority: integer(row, 'priority'),
createdAtMs: integer(row, 'createdAtMs'),
};
const optionalTexts = [
['taskName', 'taskName'],
['taskSnapshotRef', 'taskSnapshotRef'],
['parentRunId', 'parentRunId'],
['retryOfRunId', 'retryOfRunId'],
['triggerId', 'triggerId'],
['triggeredBy', 'triggeredBy'],
['requestId', 'requestId'],
['idempotencyKey', 'idempotencyKey'],
['inputRef', 'inputRef'],
['outputRef', 'outputRef'],
['errorCode', 'errorCode'],
['errorSummary', 'errorSummary'],
] as const;
for (const [property, key] of optionalTexts) {
const value = optionalText(row, key);
if (value !== undefined) {
(run as unknown as Record<string, unknown>)[property] = value;
}
}
const optionalIntegers = [
['legacyCronId', 'legacyCronId'],
['scheduledForMs', 'scheduledForMs'],
['queuedAtMs', 'queuedAtMs'],
['startedAtMs', 'startedAtMs'],
['finishedAtMs', 'finishedAtMs'],
['cancelRequestedAtMs', 'cancelRequestedAtMs'],
] as const;
for (const [property, key] of optionalIntegers) {
const value = optionalInteger(row, key);
if (value !== undefined) {
(run as unknown as Record<string, unknown>)[property] = value;
}
}
const cancelReason = optionalText(row, 'cancelReason');
if (cancelReason !== undefined) {
run.cancelReason =
cancelReason as NonNullable<RunRecord['cancelReason']>;
}
return Object.freeze(run);
}
function stepRunFromRow(row: Row): Readonly<StepRunRecord> {
let stepRun: Readonly<StepRunRecord>;
try {
stepRun = normalizeStepRunRecord(
postgresRequiredJsonObject(
row.stepRunJson,
unavailable,
) as unknown as StepRunRecord,
);
} catch {
throw unavailable();
}
if (
text(row, 'id') !== stepRun.id ||
text(row, 'runId') !== stepRun.runId ||
optionalText(row, 'parentStepRunId') !==
(stepRun.parentStepRunId ?? undefined) ||
text(row, 'stepKey') !== stepRun.stepKey ||
text(row, 'kind') !== stepRun.kind ||
text(row, 'definitionRef') !== stepRun.definitionRef ||
text(row, 'definitionDigest') !== stepRun.definitionDigest ||
postgresRequiredBoolean(row.required, unavailable) !== stepRun.required ||
text(row, 'status') !== stepRun.status ||
integer(row, 'version') !== stepRun.version ||
integer(row, 'attemptCount') !== stepRun.attemptCount ||
optionalText(row, 'inputRef') !== (stepRun.inputRef ?? undefined) ||
optionalText(row, 'outputRef') !== (stepRun.outputRef ?? undefined) ||
optionalText(row, 'approvalRequestId') !==
(stepRun.approvalRequestId ?? undefined) ||
optionalInteger(row, 'readyAtMs') !==
(stepRun.readyAtMs ?? undefined) ||
optionalInteger(row, 'startedAtMs') !==
(stepRun.startedAtMs ?? undefined) ||
optionalInteger(row, 'finishedAtMs') !==
(stepRun.finishedAtMs ?? undefined) ||
optionalText(row, 'resultCode') !==
(stepRun.resultCode ?? undefined) ||
optionalText(row, 'errorSummary') !==
(stepRun.errorSummary ?? undefined) ||
integer(row, 'createdAtMs') !== stepRun.createdAtMs ||
integer(row, 'updatedAtMs') !== stepRun.updatedAtMs ||
text(row, 'lastMutationId') !== stepRun.lastMutationId ||
text(row, 'stepRunDigest') !== stepRun.stepRunDigest
) {
throw unavailable();
}
return stepRun;
}
function planFromRow(row: Row): Readonly<PluginPackageWorkflowExecutionPlan> {
try {
return normalizePluginPackageWorkflowExecutionPlan(
postgresRequiredJsonObject(
row.planJson,
unavailable,
) as unknown as PluginPackageWorkflowExecutionPlan,
);
} catch {
throw unavailable();
}
}
function reconciliationFromRow(
row: Row,
): Readonly<PluginPackageTaskReconciliationReceipt> {
try {
return normalizePluginPackageTaskReconciliationReceipt(
postgresRequiredJsonObject(
row.reconciliationJson,
unavailable,
) as unknown as PluginPackageTaskReconciliationReceipt,
);
} catch {
throw unavailable();
}
}
function executionFromRow(
row: Row,
): Readonly<ClusterTaskExecutionRevision> {
try {
const plan = postgresRequiredJsonObject(row.executionPlanJson, unavailable);
const keys = Object.keys(plan);
if (
!keys.includes('command') ||
!keys.includes('environment') ||
keys.some(
(key) =>
![
'command',
'environment',
'placement',
'timeoutMs',
'workingDirectory',
].includes(key),
)
) {
throw unavailable();
}
return normalizeClusterTaskExecutionRevision({
projectId: text(row, 'executionProjectId'),
taskId: text(row, 'executionTaskId'),
sourceRevision: integer(row, 'executionSourceRevision'),
taskRevision: text(row, 'executionTaskRevision'),
sourceContentDigest: text(row, 'executionSourceContentDigest'),
executorType: text(
row,
'executionExecutorType',
) as ClusterTaskExecutionRevision['executorType'],
planSchema: text(
row,
'executionPlanSchema',
) as ClusterTaskExecutionRevision['planSchema'],
command: plan.command as ClusterTaskExecutionRevision['command'],
environment:
plan.environment as ClusterTaskExecutionRevision['environment'],
...(plan.workingDirectory === undefined
? {}
: { workingDirectory: plan.workingDirectory as string }),
...(plan.timeoutMs === undefined
? {}
: { timeoutMs: plan.timeoutMs as number }),
...(plan.placement === undefined
? {}
: {
placement:
plan.placement as unknown as NonNullable<
ClusterTaskExecutionRevision['placement']
>,
}),
contentDigest: text(row, 'executionContentDigest'),
createdAtMs: integer(row, 'executionCreatedAtMs'),
});
} catch (error) {
if (
error instanceof
PluginPackageWorkflowTaskAttemptAdmissionUnavailableError
) {
throw error;
}
throw unavailable();
}
}
function receiptFromRow(
row: Row,
): Readonly<PluginPackageWorkflowTaskAttemptAdmissionReceipt> {
try {
const receipt =
normalizePluginPackageWorkflowTaskAttemptAdmissionReceipt(
postgresRequiredJsonObject(
row.receiptJson,
unavailable,
) as unknown as PluginPackageWorkflowTaskAttemptAdmissionReceipt,
);
if (
row.receiptDigest !== undefined &&
text(row, 'receiptDigest') !== receipt.receiptDigest
) {
throw unavailable();
}
return receipt;
} catch (error) {
if (
error instanceof
PluginPackageWorkflowTaskAttemptAdmissionUnavailableError
) {
throw error;
}
throw unavailable();
}
}
export class PostgresPluginPackageWorkflowTaskAttemptAdmissionRepository
implements PluginPackageWorkflowTaskAttemptAdmissionRepository
{
constructor(private readonly pool: PostgresPool) {
if (!pool || typeof pool.connect !== 'function') {
throw new TypeError(
'PostgreSQL Workflow Task Attempt admission pool is invalid',
);
}
}
async listCandidates(queryValue: Readonly<{
limit: number;
after?: Readonly<PluginPackageWorkflowTaskAttemptAdmissionCursor>;
}>): Promise<
Readonly<PluginPackageWorkflowTaskAttemptAdmissionPage>
> {
if (
!queryValue ||
typeof queryValue !== 'object' ||
Array.isArray(queryValue) ||
!Reflect.has(queryValue, 'limit') ||
Reflect.ownKeys(queryValue).some(
(key) => key !== 'limit' && key !== 'after',
)
) {
throw new InvalidPluginPackageWorkflowTaskAttemptAdmissionError(
'page query is invalid',
);
}
const limit = pageLimit(queryValue.limit);
const after = cursor(queryValue.after);
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
throw unavailable(error);
}
try {
const result = await client.query<Row>(
`SELECT current.run_id AS "runId",
current.id AS "stepRunId",
current.ready_at_ms AS "readyAtMs",
admission.plan_digest AS "planDigest"
FROM "ql3"."step_runs" AS current
JOIN "ql3"."plugin_package_workflow_admission_steps" AS source
ON source.run_id = current.run_id
AND source.step_run_id = current.id
JOIN "ql3"."plugin_package_workflow_admissions" AS admission
ON admission.plan_digest = source.plan_digest
AND admission.run_id = source.run_id
JOIN "ql3"."runs" AS run ON run.id = current.run_id
WHERE run.status = 'running'
AND run.cancel_requested_at_ms IS NULL
AND current.kind = 'task'
AND current.status = 'ready'
AND current.ready_at_ms IS NOT NULL
AND current.attempt_count < 64
AND NOT EXISTS (
SELECT 1
FROM "ql3"."plugin_package_workflow_task_attempt_admissions"
AS task_attempt
WHERE task_attempt.run_id = current.run_id
AND task_attempt.step_run_id = current.id
AND task_attempt.step_run_version = current.version
)
AND (
$1::varchar IS NULL OR current.ready_at_ms > $2 OR
(current.ready_at_ms = $2 AND current.id > $1)
)
ORDER BY current.ready_at_ms, current.id
LIMIT $3`,
[
after?.stepRunId ?? null,
after?.readyAtMs ?? 0,
limit + 1,
],
);
const mapped = result.rows.map(
(row): Readonly<PluginPackageWorkflowTaskAttemptAdmissionCandidate> =>
Object.freeze({
runId: identity(text(row, 'runId'), 'candidate runId'),
stepRunId: identity(
text(row, 'stepRunId'),
'candidate stepRunId',
),
readyAtMs: integer(row, 'readyAtMs'),
planDigest: digest(row.planDigest),
}),
);
const truncated = mapped.length > limit;
const candidates = Object.freeze(mapped.slice(0, limit));
const last = candidates.at(-1);
return Object.freeze({
candidates,
truncated,
...(truncated && last
? {
next: Object.freeze({
readyAtMs: last.readyAtMs,
stepRunId: last.stepRunId,
}),
}
: {}),
});
} catch (error) {
throw mapStorageError(error);
} finally {
client.release();
}
}
async admit(
runIdValue: string,
stepRunIdValue: string,
): Promise<
Readonly<PluginPackageWorkflowTaskAttemptAdmissionResult>
> {
const runId = identity(runIdValue, 'runId');
const stepRunId = identity(stepRunIdValue, 'stepRunId');
for (
let transactionAttempt = 0;
transactionAttempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
transactionAttempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
throw unavailable(error);
}
let began = false;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const runRows = await client.query<Row>(
`SELECT ${RUN_SELECT}
FROM "ql3"."runs" WHERE id = $1 LIMIT 2 FOR UPDATE`,
[runId],
);
if (runRows.rows.length !== 1) {
throw new PluginPackageWorkflowTaskAttemptAdmissionConflictError();
}
const run = runFromRow(runRows.rows[0]!);
const stepRows = await client.query<Row>(
`SELECT ${STEP_RUN_SELECT}
FROM "ql3"."step_runs"
WHERE run_id = $1 AND id = $2 LIMIT 2 FOR UPDATE`,
[runId, stepRunId],
);
if (stepRows.rows.length !== 1) {
throw new PluginPackageWorkflowTaskAttemptAdmissionConflictError();
}
const stepRun = stepRunFromRow(stepRows.rows[0]!);
const existing = await client.query<Row>(
`SELECT receipt_digest AS "receiptDigest",
receipt_json AS "receiptJson"
FROM "ql3"."plugin_package_workflow_task_attempt_admissions"
WHERE run_id = $1 AND step_run_id = $2
AND step_run_version = $3
LIMIT 2`,
[runId, stepRunId, stepRun.version],
);
if (existing.rows.length > 1) throw unavailable();
if (existing.rows.length === 1) {
const receipt = receiptFromRow(existing.rows[0]!);
if (
receipt.runId !== runId ||
receipt.stepRunId !== stepRunId ||
receipt.stepRunVersion !== stepRun.version ||
receipt.stepRunDigest !== stepRun.stepRunDigest
) {
throw unavailable();
}
await client.query('COMMIT');
began = false;
return Object.freeze({
status: 'existing' as const,
receipt,
});
}
if (stepRun.status !== 'ready') {
throw new PluginPackageWorkflowTaskAttemptAdmissionConflictError();
}
const snapshot = await client.query<Row>(
`SELECT plan_json AS "planJson",
reconciliation_json AS "reconciliationJson",
execution_project_id AS "executionProjectId",
execution_task_id AS "executionTaskId",
execution_source_revision AS "executionSourceRevision",
execution_task_revision AS "executionTaskRevision",
execution_source_content_digest
AS "executionSourceContentDigest",
execution_executor_type AS "executionExecutorType",
execution_plan_schema AS "executionPlanSchema",
execution_plan_json AS "executionPlanJson",
execution_content_digest AS "executionContentDigest",
execution_created_at_ms AS "executionCreatedAtMs"
FROM "ql3"."plugin_package_workflow_task_attempt_snapshot"($1, $2)`,
[runId, stepRunId],
);
if (snapshot.rows.length !== 1) {
throw new PluginPackageWorkflowTaskAttemptAdmissionConflictError();
}
const snapshotRow = snapshot.rows[0]!;
const plan = planFromRow(snapshotRow);
const taskReconciliation = reconciliationFromRow(snapshotRow);
const execution = executionFromRow(snapshotRow);
const clock = await client.query<Row>(
`SELECT floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint AS "admittedAtMs"`,
);
if (clock.rows.length !== 1) throw unavailable();
const attemptNumberRows = await client.query<Row>(
`SELECT COALESCE(MAX(attempt), 0) + 1 AS "attemptNumber"
FROM "ql3"."run_attempts" WHERE run_id = $1`,
[runId],
);
if (attemptNumberRows.rows.length !== 1) throw unavailable();
const bundle = createPluginPackageWorkflowTaskAttemptAdmission({
plan,
run,
stepRun,
taskReconciliation,
execution,
attemptNumber: integer(
attemptNumberRows.rows[0]!,
'attemptNumber',
),
admittedAtMs: integer(clock.rows[0]!, 'admittedAtMs'),
});
const updated = await client.query(
`UPDATE "ql3"."runs"
SET version = $1, event_sequence = $2
WHERE id = $3 AND status = 'running'
AND cancel_requested_at_ms IS NULL
AND version = $4 AND event_sequence = $5`,
[
bundle.run.version,
bundle.run.eventSequence,
run.id,
run.version,
run.eventSequence,
],
);
if (updated.rowCount !== 1) {
throw new PluginPackageWorkflowTaskAttemptAdmissionConflictError();
}
const attempt = bundle.attempt;
await client.query(
`INSERT INTO "ql3"."run_attempts" (
id, run_id, step_run_id, attempt, status, executor_type,
callback_sequence, created_at_ms
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
[
attempt.id,
attempt.runId,
attempt.stepRunId ?? null,
attempt.attempt,
attempt.status,
attempt.executorType,
attempt.callbackSequence,
attempt.createdAtMs,
],
);
const event = bundle.event;
await client.query(
`INSERT INTO "ql3"."run_events" (
id, run_id, sequence, type, dedupe_key, actor_type, actor_id,
attempt_id, step_run_id, payload, created_at_ms
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11
)`,
[
event.id,
event.runId,
event.sequence,
event.type,
event.dedupeKey ?? null,
event.actorType,
event.actorId ?? null,
event.attemptId ?? null,
event.stepRunId ?? null,
JSON.stringify(event.payload),
event.createdAtMs,
],
);
const receipt = bundle.receipt;
await client.query(
`INSERT INTO
"ql3"."plugin_package_workflow_task_attempt_admissions" (
receipt_digest, attempt_id, plan_digest, run_id,
step_run_id, step_run_version, step_run_digest,
generation_digest, resource_task_id,
task_reconciliation_receipt_digest, project_id, task_id,
source_revision, task_revision, task_definition_digest,
executor_type, execution_digest, attempt_number, event_id,
run_version, run_event_sequence, admitted_at_ms, receipt_json
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
$14, $15, $16, $17, $18, $19, $20, $21, $22, $23::jsonb
)`,
[
receipt.receiptDigest,
receipt.attemptId,
receipt.planDigest,
receipt.runId,
receipt.stepRunId,
receipt.stepRunVersion,
receipt.stepRunDigest,
plan.target.generationDigest,
receipt.resourceTaskId,
receipt.taskReconciliationReceiptDigest,
execution.projectId,
receipt.taskId,
execution.sourceRevision,
receipt.taskRevision,
receipt.taskDefinitionDigest,
receipt.executorType,
receipt.executionDigest,
receipt.attemptNumber,
receipt.eventId,
receipt.runVersion,
receipt.runEventSequence,
receipt.admittedAtMs,
JSON.stringify(receipt),
],
);
await client.query('COMMIT');
began = false;
return Object.freeze({
status: 'created' as const,
receipt,
});
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
state &&
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
transactionAttempt + 1 <
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
) {
continue;
}
throw mapStorageError(error);
} finally {
client.release();
}
}
throw unavailable();
}
}

Some files were not shown because too many files have changed in this diff Show More