mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,484 @@
|
||||
import type { ApiCredentialRepository } from '@qinglong/runtime-core/api-credential';
|
||||
import type { LocalOwnerPepperRepository } from '@qinglong/runtime-core/local-owner-pepper';
|
||||
import type { PluginPackageQuarantineRepository } from '@qinglong/runtime-core/plugin-package-quarantine';
|
||||
import type { PluginPackageAutomationPublicationRepository } from '@qinglong/runtime-core/plugin-package-automation-publication';
|
||||
import type { LocalSecretEnvelopeRepository } from '@qinglong/runtime-core/local-secret';
|
||||
import type { ProjectPolicyRepository } from '@qinglong/runtime-core/project-policy';
|
||||
import type { SecurityAuditSink } from '@qinglong/runtime-core/security-audit';
|
||||
import type { SecurityAuditRecord } from '@qinglong/runtime-core/security-audit';
|
||||
import type {
|
||||
SecurityPolicyFence,
|
||||
SecuritySubject,
|
||||
} from '@qinglong/runtime-core/security';
|
||||
|
||||
import { LocalSqliteApiCredentialRepository } from '../security/apiCredentialRepository';
|
||||
import {
|
||||
assertLocalSqliteOptions,
|
||||
assertLocalSqlitePathBoundary,
|
||||
LocalSqliteConfigurationError,
|
||||
openLocalSqliteClient,
|
||||
type LocalSqliteDatabaseOptions,
|
||||
type LocalSqliteProfile,
|
||||
} from '../storage/config';
|
||||
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
|
||||
import { LocalSqliteOwnerPepperRepository } from '../local-owner/ownerPepperRepository';
|
||||
import {
|
||||
EDGE_PLUGIN_PACKAGE_QUARANTINE_ACTIVE_SOURCE_LIMIT,
|
||||
LocalSqlitePluginPackageQuarantineRepository,
|
||||
STANDALONE_PLUGIN_PACKAGE_QUARANTINE_ACTIVE_SOURCE_LIMIT,
|
||||
} from '../plugin-package/pluginPackageQuarantineRepository';
|
||||
import { LocalSqlitePluginPackageAutomationPublicationRepository } from '../plugin-package/pluginPackageAutomationPublicationRepository';
|
||||
import {
|
||||
insertLocalSecurityAudit,
|
||||
localSecurityAuditFromRow,
|
||||
sameSecurityAuditSemantic,
|
||||
} from '../security/securityPersistence';
|
||||
import { LocalSqliteSecurityAuthorityStore } from '../security/securityAuthorityStore';
|
||||
import {
|
||||
auditLocalSqliteReadiness,
|
||||
type LocalSqliteReadinessEvidence,
|
||||
} from '../readiness/readiness';
|
||||
|
||||
/**
|
||||
* One short-lived local authority for authenticated package management.
|
||||
* It deliberately stays behind an explicit subpath and never migrates schema.
|
||||
*/
|
||||
export interface LocalSqlitePluginPackageManagementDatabase {
|
||||
readonly profile: LocalSqliteProfile;
|
||||
readonly readiness: LocalSqliteReadinessEvidence;
|
||||
readonly authority: LocalSqliteOperationAuthority;
|
||||
readonly apiCredentials: ApiCredentialRepository;
|
||||
readonly ownerPepper: Pick<LocalOwnerPepperRepository, 'resolveKey'>;
|
||||
readonly pluginPackageQuarantine: PluginPackageQuarantineRepository;
|
||||
readonly securityAudit: SecurityAuditSink;
|
||||
confirmUserCredentialFence(
|
||||
fence: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
|
||||
): void;
|
||||
confirmDefaultProjectOwnerFence(
|
||||
fence: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
|
||||
): void;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface LocalSqliteOptionalFeatureRuntimeDatabase {
|
||||
readonly profile: LocalSqliteProfile;
|
||||
readonly readiness: LocalSqliteReadinessEvidence;
|
||||
readonly authority: LocalSqliteOperationAuthority;
|
||||
readonly apiCredentials: ApiCredentialRepository;
|
||||
readonly ownerPepper: Pick<LocalOwnerPepperRepository, 'resolveKey'>;
|
||||
readonly projectPolicy: ProjectPolicyRepository;
|
||||
readonly automationPublications: Pick<
|
||||
PluginPackageAutomationPublicationRepository,
|
||||
'findCurrent' | 'findByDigest'
|
||||
>;
|
||||
readonly localSecrets: LocalSecretEnvelopeRepository;
|
||||
readonly securityAudit: SecurityAuditSink;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface LocalSqliteAuthenticatedUserCredentialFence {
|
||||
readonly credentialId: string;
|
||||
readonly credentialVersion: number;
|
||||
readonly pepperKeyId: string;
|
||||
readonly materialDigest: string;
|
||||
readonly subjectType: 'user';
|
||||
readonly subjectId: string;
|
||||
readonly secretDigest: string;
|
||||
readonly notBeforeAtMs: number;
|
||||
readonly expiresAtMs: number;
|
||||
}
|
||||
|
||||
export class LocalSqliteAuthenticatedManagementFenceError extends Error {
|
||||
readonly code = 'LOCAL_SQLITE_AUTHENTICATED_MANAGEMENT_FENCE_REJECTED';
|
||||
|
||||
constructor() {
|
||||
super('The authenticated local management credential fence was rejected');
|
||||
this.name = 'LocalSqliteAuthenticatedManagementFenceError';
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalSqliteAuthenticatedManagementOwnerError extends Error {
|
||||
readonly code = 'LOCAL_SQLITE_AUTHENTICATED_MANAGEMENT_OWNER_REJECTED';
|
||||
|
||||
constructor() {
|
||||
super('The authenticated local management User is not a current Owner');
|
||||
this.name = 'LocalSqliteAuthenticatedManagementOwnerError';
|
||||
}
|
||||
}
|
||||
|
||||
function exactFence(
|
||||
value: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
|
||||
): void {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
Object.keys(value).sort().join('\0') !==
|
||||
[
|
||||
'credentialId',
|
||||
'credentialVersion',
|
||||
'expiresAtMs',
|
||||
'materialDigest',
|
||||
'notBeforeAtMs',
|
||||
'pepperKeyId',
|
||||
'secretDigest',
|
||||
'subjectId',
|
||||
'subjectType',
|
||||
]
|
||||
.sort()
|
||||
.join('\0') ||
|
||||
typeof value.credentialId !== 'string' ||
|
||||
value.credentialId.length < 1 ||
|
||||
value.credentialId.length > 128 ||
|
||||
!Number.isSafeInteger(value.credentialVersion) ||
|
||||
value.credentialVersion < 1 ||
|
||||
typeof value.pepperKeyId !== 'string' ||
|
||||
value.pepperKeyId.length < 1 ||
|
||||
value.pepperKeyId.length > 128 ||
|
||||
!/^[0-9a-f]{64}$/.test(value.materialDigest) ||
|
||||
value.subjectType !== 'user' ||
|
||||
typeof value.subjectId !== 'string' ||
|
||||
value.subjectId.length < 1 ||
|
||||
value.subjectId.length > 255 ||
|
||||
!/^[0-9a-f]{64}$/.test(value.secretDigest) ||
|
||||
!Number.isSafeInteger(value.notBeforeAtMs) ||
|
||||
value.notBeforeAtMs < 0 ||
|
||||
!Number.isSafeInteger(value.expiresAtMs) ||
|
||||
value.expiresAtMs <= value.notBeforeAtMs
|
||||
) {
|
||||
throw new LocalSqliteAuthenticatedManagementFenceError();
|
||||
}
|
||||
}
|
||||
|
||||
function confirmUserCredentialFence(
|
||||
authority: LocalSqliteOperationAuthority,
|
||||
fence: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
|
||||
): void {
|
||||
exactFence(fence);
|
||||
try {
|
||||
const row = authority.client
|
||||
.prepare(
|
||||
`SELECT
|
||||
credential."state" AS "credentialState",
|
||||
credential."subject_type" AS "subjectType",
|
||||
credential."subject_id" AS "subjectId",
|
||||
credential."secret_digest" AS "secretDigest",
|
||||
credential."not_before_at_ms" AS "notBeforeAtMs",
|
||||
credential."expires_at_ms" AS "expiresAtMs",
|
||||
identity."status" AS "subjectStatus",
|
||||
binding."pepper_key_id" AS "pepperKeyId",
|
||||
pepper."state" AS "pepperState",
|
||||
pepper."material_digest" AS "materialDigest",
|
||||
CAST(unixepoch('subsec') * 1000 AS INTEGER) AS "nowMs"
|
||||
FROM "QingLong3ApiCredentials" AS credential
|
||||
JOIN "QingLong3IdentitySubjects" AS identity
|
||||
ON identity."subject_type" = credential."subject_type"
|
||||
AND identity."subject_id" = credential."subject_id"
|
||||
JOIN "QingLong3ApiCredentialPepperBindings" AS binding
|
||||
ON binding."credential_id" = credential."credential_id"
|
||||
AND binding."credential_version" = credential."version"
|
||||
JOIN "QingLong3LocalOwnerPepperKeys" AS pepper
|
||||
ON pepper."pepper_key_id" = binding."pepper_key_id"
|
||||
WHERE credential."credential_id" = ?
|
||||
AND credential."version" = ?`,
|
||||
)
|
||||
.get(fence.credentialId, fence.credentialVersion) as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
if (
|
||||
!row ||
|
||||
row.credentialState !== 'active' ||
|
||||
row.subjectStatus !== 'active' ||
|
||||
row.subjectType !== fence.subjectType ||
|
||||
row.subjectId !== fence.subjectId ||
|
||||
row.secretDigest !== fence.secretDigest ||
|
||||
row.pepperKeyId !== fence.pepperKeyId ||
|
||||
(row.pepperState !== 'active' && row.pepperState !== 'retired') ||
|
||||
row.materialDigest !== fence.materialDigest ||
|
||||
row.notBeforeAtMs !== fence.notBeforeAtMs ||
|
||||
row.expiresAtMs !== fence.expiresAtMs ||
|
||||
!Number.isSafeInteger(row.nowMs) ||
|
||||
(row.nowMs as number) < fence.notBeforeAtMs ||
|
||||
(row.nowMs as number) >= fence.expiresAtMs
|
||||
) {
|
||||
throw new LocalSqliteAuthenticatedManagementFenceError();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof LocalSqliteAuthenticatedManagementFenceError) {
|
||||
throw error;
|
||||
}
|
||||
throw new LocalSqliteAuthenticatedManagementFenceError();
|
||||
}
|
||||
}
|
||||
|
||||
export function confirmLocalSqliteAuthenticatedUserCredentialFence(
|
||||
authority: LocalSqliteOperationAuthority,
|
||||
fence: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
|
||||
): void {
|
||||
confirmUserCredentialFence(authority, fence);
|
||||
}
|
||||
|
||||
export function confirmLocalSqliteProjectPolicyFence(
|
||||
authority: LocalSqliteOperationAuthority,
|
||||
projectId: string,
|
||||
actor: Readonly<SecuritySubject>,
|
||||
fence: Readonly<SecurityPolicyFence>,
|
||||
): void {
|
||||
try {
|
||||
if (
|
||||
!Number.isSafeInteger(fence.projectVersion) ||
|
||||
fence.projectVersion < 1 ||
|
||||
!Number.isSafeInteger(fence.bindingVersion) ||
|
||||
(fence.bindingVersion as number) < 1
|
||||
) {
|
||||
throw new LocalSqliteAuthenticatedManagementFenceError();
|
||||
}
|
||||
const row = authority.client
|
||||
.prepare(
|
||||
`SELECT project."status" AS "projectStatus",
|
||||
project."version" AS "projectVersion",
|
||||
binding."state" AS "bindingState",
|
||||
binding."version" AS "bindingVersion"
|
||||
FROM "QingLong3Projects" AS project
|
||||
JOIN "QingLong3ProjectRoleBindings" AS binding
|
||||
ON binding."project_id" = project."id"
|
||||
AND binding."subject_type" = ?
|
||||
AND binding."subject_id" = ?
|
||||
WHERE project."id" = ?
|
||||
AND binding."version" = (
|
||||
SELECT max(latest."version")
|
||||
FROM "QingLong3ProjectRoleBindings" AS latest
|
||||
WHERE latest."project_id" = binding."project_id"
|
||||
AND latest."subject_type" = binding."subject_type"
|
||||
AND latest."subject_id" = binding."subject_id"
|
||||
)`,
|
||||
)
|
||||
.get(actor.type, actor.id, projectId) as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
if (
|
||||
!row ||
|
||||
row.projectStatus !== 'active' ||
|
||||
row.bindingState !== 'active' ||
|
||||
row.projectVersion !== fence.projectVersion ||
|
||||
row.bindingVersion !== fence.bindingVersion
|
||||
) {
|
||||
throw new LocalSqliteAuthenticatedManagementFenceError();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof LocalSqliteAuthenticatedManagementFenceError) {
|
||||
throw error;
|
||||
}
|
||||
throw new LocalSqliteAuthenticatedManagementFenceError();
|
||||
}
|
||||
}
|
||||
|
||||
const AUTHENTICATED_AUDIT_SELECT = `
|
||||
"event_id" AS "eventId",
|
||||
"request_id" AS "requestId",
|
||||
"operation_id" AS "operationId",
|
||||
"project_id" AS "auditProjectId",
|
||||
"subject_type" AS "subjectType",
|
||||
"subject_id" AS "subjectId",
|
||||
"authentication_id" AS "authenticationId",
|
||||
"outcome" AS "outcome",
|
||||
"reasons_json" AS "reasonsJson",
|
||||
"fence_project_version" AS "fenceProjectVersion",
|
||||
"fence_binding_version" AS "fenceBindingVersion",
|
||||
"occurred_at_ms" AS "occurredAtMs"`;
|
||||
|
||||
export function commitLocalSqliteSecurityAuditInTransaction(
|
||||
authority: LocalSqliteOperationAuthority,
|
||||
audit: Readonly<SecurityAuditRecord>,
|
||||
replay: boolean,
|
||||
): void {
|
||||
try {
|
||||
if (!authority.client.isTransaction) {
|
||||
throw new LocalSqliteAuthenticatedManagementFenceError();
|
||||
}
|
||||
const row = authority.client
|
||||
.prepare(
|
||||
`SELECT ${AUTHENTICATED_AUDIT_SELECT}
|
||||
FROM "QingLong3SecurityAuditEvents" WHERE "event_id" = ?`,
|
||||
)
|
||||
.get(audit.eventId) as Record<string, unknown> | undefined;
|
||||
if (replay) {
|
||||
const stored = row ? localSecurityAuditFromRow(row) : null;
|
||||
const storedWithoutTime = stored
|
||||
? Object.freeze({ ...stored, occurredAtMs: audit.occurredAtMs })
|
||||
: null;
|
||||
if (
|
||||
!storedWithoutTime ||
|
||||
!sameSecurityAuditSemantic(storedWithoutTime, audit)
|
||||
) {
|
||||
throw new LocalSqliteAuthenticatedManagementFenceError();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (row) throw new LocalSqliteAuthenticatedManagementFenceError();
|
||||
insertLocalSecurityAudit(authority.client, audit);
|
||||
} catch (error) {
|
||||
if (error instanceof LocalSqliteAuthenticatedManagementFenceError) {
|
||||
throw error;
|
||||
}
|
||||
throw new LocalSqliteAuthenticatedManagementFenceError();
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDefaultProjectOwnerFence(
|
||||
authority: LocalSqliteOperationAuthority,
|
||||
fence: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
|
||||
): void {
|
||||
exactFence(fence);
|
||||
try {
|
||||
const row = authority.client
|
||||
.prepare(
|
||||
`SELECT
|
||||
project."status" AS "projectStatus",
|
||||
binding."state" AS "bindingState",
|
||||
binding."role" AS "role"
|
||||
FROM "QingLong3Projects" AS project
|
||||
JOIN "QingLong3ProjectRoleBindings" AS binding
|
||||
ON binding."project_id" = project."id"
|
||||
AND binding."subject_type" = ?
|
||||
AND binding."subject_id" = ?
|
||||
WHERE project."id" = 'default'
|
||||
AND binding."version" = (
|
||||
SELECT max(latest."version")
|
||||
FROM "QingLong3ProjectRoleBindings" AS latest
|
||||
WHERE latest."project_id" = binding."project_id"
|
||||
AND latest."subject_type" = binding."subject_type"
|
||||
AND latest."subject_id" = binding."subject_id"
|
||||
)`,
|
||||
)
|
||||
.get(fence.subjectType, fence.subjectId) as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
if (
|
||||
!row ||
|
||||
row.projectStatus !== 'active' ||
|
||||
row.bindingState !== 'active' ||
|
||||
row.role !== 'owner'
|
||||
) {
|
||||
throw new LocalSqliteAuthenticatedManagementOwnerError();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof LocalSqliteAuthenticatedManagementOwnerError) {
|
||||
throw error;
|
||||
}
|
||||
throw new LocalSqliteAuthenticatedManagementOwnerError();
|
||||
}
|
||||
}
|
||||
|
||||
export async function openLocalSqlitePluginPackageManagementDatabase(
|
||||
options: LocalSqliteDatabaseOptions,
|
||||
): Promise<LocalSqlitePluginPackageManagementDatabase> {
|
||||
assertLocalSqliteOptions(options);
|
||||
assertLocalSqlitePathBoundary(options.databasePath, false);
|
||||
const client = openLocalSqliteClient(options, false);
|
||||
try {
|
||||
const readiness = await auditLocalSqliteReadiness(client);
|
||||
const authority = new LocalSqliteOperationAuthority(client);
|
||||
const apiCredentials = new LocalSqliteApiCredentialRepository(authority);
|
||||
const ownerPepper = new LocalSqliteOwnerPepperRepository(authority);
|
||||
const pluginPackageQuarantine =
|
||||
new LocalSqlitePluginPackageQuarantineRepository(authority, {
|
||||
activeSourceLimit:
|
||||
options.profile === 'edge'
|
||||
? EDGE_PLUGIN_PACKAGE_QUARANTINE_ACTIVE_SOURCE_LIMIT
|
||||
: STANDALONE_PLUGIN_PACKAGE_QUARANTINE_ACTIVE_SOURCE_LIMIT,
|
||||
});
|
||||
const securityAudit = new LocalSqliteSecurityAuthorityStore(authority);
|
||||
let closePromise: Promise<void> | undefined;
|
||||
return Object.freeze({
|
||||
profile: options.profile,
|
||||
readiness,
|
||||
authority,
|
||||
apiCredentials,
|
||||
ownerPepper,
|
||||
pluginPackageQuarantine,
|
||||
securityAudit,
|
||||
confirmUserCredentialFence(
|
||||
fence: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
|
||||
) {
|
||||
confirmUserCredentialFence(authority, fence);
|
||||
},
|
||||
confirmDefaultProjectOwnerFence(
|
||||
fence: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
|
||||
) {
|
||||
confirmDefaultProjectOwnerFence(authority, fence);
|
||||
},
|
||||
close() {
|
||||
if (closePromise) return closePromise;
|
||||
closePromise = authority.close();
|
||||
return closePromise;
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (client.isOpen) client.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens one explicit optional-feature runtime authority without loading any
|
||||
* feature implementation. Callers must inspect their durable feature head
|
||||
* before importing the optional package and must close this authority on every
|
||||
* inactive, failed, or stopped path.
|
||||
*/
|
||||
export async function openLocalSqliteOptionalFeatureRuntimeDatabase(
|
||||
options: LocalSqliteDatabaseOptions,
|
||||
): Promise<LocalSqliteOptionalFeatureRuntimeDatabase> {
|
||||
assertLocalSqliteOptions(options);
|
||||
assertLocalSqlitePathBoundary(options.databasePath, false);
|
||||
const client = openLocalSqliteClient(options, false);
|
||||
try {
|
||||
const readiness = await auditLocalSqliteReadiness(client);
|
||||
const authority = new LocalSqliteOperationAuthority(client);
|
||||
const securityAuthority = new LocalSqliteSecurityAuthorityStore(authority);
|
||||
const projectPolicy: ProjectPolicyRepository = Object.freeze({
|
||||
resolve: (
|
||||
...[projectId, subject]: Parameters<ProjectPolicyRepository['resolve']>
|
||||
) => securityAuthority.resolve(projectId, subject),
|
||||
append: (...[command]: Parameters<ProjectPolicyRepository['append']>) =>
|
||||
securityAuthority.append(command),
|
||||
});
|
||||
let closePromise: Promise<void> | undefined;
|
||||
return Object.freeze({
|
||||
profile: options.profile,
|
||||
readiness,
|
||||
authority,
|
||||
apiCredentials: new LocalSqliteApiCredentialRepository(authority),
|
||||
ownerPepper: new LocalSqliteOwnerPepperRepository(authority),
|
||||
projectPolicy,
|
||||
automationPublications:
|
||||
new LocalSqlitePluginPackageAutomationPublicationRepository(authority),
|
||||
localSecrets: securityAuthority,
|
||||
securityAudit: securityAuthority,
|
||||
close() {
|
||||
if (closePromise) return closePromise;
|
||||
closePromise = authority.close();
|
||||
return closePromise;
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (client.isOpen) client.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Neutral names for shared short-lived authenticated management ceremonies.
|
||||
* The historical Plugin Package names remain compatible aliases.
|
||||
*/
|
||||
export type LocalSqliteAuthenticatedManagementDatabase =
|
||||
LocalSqlitePluginPackageManagementDatabase;
|
||||
export const openLocalSqliteAuthenticatedManagementDatabase =
|
||||
openLocalSqlitePluginPackageManagementDatabase;
|
||||
|
||||
export {
|
||||
LocalSqliteConfigurationError,
|
||||
type LocalSqliteDatabaseOptions,
|
||||
type LocalSqliteProfile,
|
||||
};
|
||||
export type { LocalSqliteReadinessEvidence } from '../readiness/readiness';
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,126 @@
|
||||
import type { ApiCredentialRepository } from '@qinglong/runtime-core/api-credential';
|
||||
import type { LocalOwnerPepperRepository } from '@qinglong/runtime-core/local-owner-pepper';
|
||||
import type { LocalSecretAdministrationRepository } from '@qinglong/runtime-core/local-secret-administration';
|
||||
import type { ProjectPolicyRepository } from '@qinglong/runtime-core/project-policy';
|
||||
import type { SecurityAuditSink } from '@qinglong/runtime-core/security-audit';
|
||||
|
||||
import { LocalSqliteApiCredentialRepository } from '../security/apiCredentialRepository';
|
||||
import {
|
||||
assertLocalSqliteOptions,
|
||||
assertLocalSqlitePathBoundary,
|
||||
LocalSqliteConfigurationError,
|
||||
openLocalSqliteClient,
|
||||
type LocalSqliteDatabaseOptions,
|
||||
type LocalSqliteProfile,
|
||||
} from '../storage/config';
|
||||
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
|
||||
import { LocalSqliteOwnerPepperRepository } from '../local-owner/ownerPepperRepository';
|
||||
import {
|
||||
confirmLocalSqliteAuthenticatedUserCredentialFence,
|
||||
LocalSqliteAuthenticatedManagementFenceError,
|
||||
type LocalSqliteAuthenticatedUserCredentialFence,
|
||||
} from './packageManagement';
|
||||
import {
|
||||
auditLocalSqliteReadiness,
|
||||
type LocalSqliteReadinessEvidence,
|
||||
} from '../readiness/readiness';
|
||||
import { LocalSqliteSecurityAuthorityStore } from '../security/securityAuthorityStore';
|
||||
|
||||
/**
|
||||
* One short-lived authority for authenticated Secret creation and rotation.
|
||||
* It deliberately excludes task execution, scheduling, plugin management and
|
||||
* schema migration capabilities.
|
||||
*/
|
||||
export interface LocalSqliteSecretAdministrationDatabase {
|
||||
readonly profile: LocalSqliteProfile;
|
||||
readonly readiness: LocalSqliteReadinessEvidence;
|
||||
readonly apiCredentials: ApiCredentialRepository;
|
||||
readonly ownerPepper: Pick<LocalOwnerPepperRepository, 'resolveKey'>;
|
||||
readonly projectPolicy: ProjectPolicyRepository;
|
||||
readonly localSecretAdministration: LocalSecretAdministrationRepository;
|
||||
readonly securityAudit: SecurityAuditSink;
|
||||
activateUserCredentialFence(
|
||||
fence: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
|
||||
): void;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
function sameFence(
|
||||
left: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
|
||||
right: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
|
||||
): boolean {
|
||||
return (
|
||||
left.credentialId === right.credentialId &&
|
||||
left.credentialVersion === right.credentialVersion &&
|
||||
left.pepperKeyId === right.pepperKeyId &&
|
||||
left.materialDigest === right.materialDigest &&
|
||||
left.subjectType === right.subjectType &&
|
||||
left.subjectId === right.subjectId &&
|
||||
left.secretDigest === right.secretDigest &&
|
||||
left.notBeforeAtMs === right.notBeforeAtMs &&
|
||||
left.expiresAtMs === right.expiresAtMs
|
||||
);
|
||||
}
|
||||
|
||||
export async function openLocalSqliteSecretAdministrationDatabase(
|
||||
options: LocalSqliteDatabaseOptions,
|
||||
): Promise<LocalSqliteSecretAdministrationDatabase> {
|
||||
assertLocalSqliteOptions(options);
|
||||
assertLocalSqlitePathBoundary(options.databasePath, false);
|
||||
const client = openLocalSqliteClient(options, false);
|
||||
try {
|
||||
const readiness = await auditLocalSqliteReadiness(client);
|
||||
const authority = new LocalSqliteOperationAuthority(client);
|
||||
let activeFence:
|
||||
| Readonly<LocalSqliteAuthenticatedUserCredentialFence>
|
||||
| undefined;
|
||||
const securityAuthority = new LocalSqliteSecurityAuthorityStore(authority, {
|
||||
beforeAuthorizedLocalSecretMutation() {
|
||||
if (!activeFence) {
|
||||
throw new LocalSqliteAuthenticatedManagementFenceError();
|
||||
}
|
||||
confirmLocalSqliteAuthenticatedUserCredentialFence(
|
||||
authority,
|
||||
activeFence,
|
||||
);
|
||||
},
|
||||
});
|
||||
const apiCredentials = new LocalSqliteApiCredentialRepository(authority);
|
||||
const ownerPepper = new LocalSqliteOwnerPepperRepository(authority);
|
||||
const projectPolicy: ProjectPolicyRepository = Object.freeze({
|
||||
resolve: (
|
||||
...[projectId, subject]: Parameters<ProjectPolicyRepository['resolve']>
|
||||
) => securityAuthority.resolve(projectId, subject),
|
||||
append: (...[command]: Parameters<ProjectPolicyRepository['append']>) =>
|
||||
securityAuthority.append(command),
|
||||
});
|
||||
let closePromise: Promise<void> | undefined;
|
||||
return Object.freeze({
|
||||
profile: options.profile,
|
||||
readiness,
|
||||
apiCredentials,
|
||||
ownerPepper,
|
||||
projectPolicy,
|
||||
localSecretAdministration: securityAuthority,
|
||||
securityAudit: securityAuthority,
|
||||
activateUserCredentialFence(
|
||||
fence: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
|
||||
) {
|
||||
confirmLocalSqliteAuthenticatedUserCredentialFence(authority, fence);
|
||||
if (activeFence && !sameFence(activeFence, fence)) {
|
||||
throw new LocalSqliteAuthenticatedManagementFenceError();
|
||||
}
|
||||
activeFence = Object.freeze({ ...fence });
|
||||
},
|
||||
close() {
|
||||
if (closePromise) return closePromise;
|
||||
closePromise = authority.close();
|
||||
return closePromise;
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (client.isOpen) client.close();
|
||||
if (error instanceof LocalSqliteConfigurationError) throw error;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,738 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import type { DatabaseSync } from 'node:sqlite';
|
||||
import {
|
||||
createTaskDefinitionRecord,
|
||||
normalizeAppendTaskDefinitionRevisionCommand,
|
||||
type TaskDefinitionSpec,
|
||||
} from '@qinglong/runtime-core/task-definition';
|
||||
import {
|
||||
BUILT_IN_COMMAND_TASK_SPEC_SCHEMA,
|
||||
createBuiltInTaskSpecSemanticRegistry,
|
||||
} from '@qinglong/runtime-core/task-spec-semantic';
|
||||
import {
|
||||
BUILT_IN_CRON_TRIGGER_SPEC_SCHEMA,
|
||||
createBuiltInTriggerSpecSemanticRegistry,
|
||||
createTriggerRecord,
|
||||
normalizeAppendTriggerRevisionCommand,
|
||||
type TriggerSpec,
|
||||
} from '@qinglong/runtime-core/trigger';
|
||||
import {
|
||||
normalizeProjectPolicySubject,
|
||||
type ProjectPolicyRepository,
|
||||
} from '@qinglong/runtime-core/project-policy';
|
||||
import type {
|
||||
SecurityPolicyFence,
|
||||
SecuritySubject,
|
||||
} from '@qinglong/runtime-core/security';
|
||||
import {
|
||||
normalizeSecurityAuditRecord,
|
||||
type SecurityAuditRecord,
|
||||
type SecurityAuditSink,
|
||||
} from '@qinglong/runtime-core/security-audit';
|
||||
|
||||
import {
|
||||
assertLocalSqliteOptions,
|
||||
assertLocalSqlitePathBoundary,
|
||||
openLocalSqliteClient,
|
||||
type LocalSqliteDatabaseOptions,
|
||||
type LocalSqliteProfile,
|
||||
} from '../storage/config';
|
||||
import { LocalSqliteDispatchDefinitionStore } from '../task-definition/dispatchDefinitionStore';
|
||||
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
|
||||
import {
|
||||
auditLocalSqliteReadiness,
|
||||
type LocalSqliteReadinessEvidence,
|
||||
} from '../readiness/readiness';
|
||||
import { LocalSqliteSecurityAuthorityStore } from '../security/securityAuthorityStore';
|
||||
|
||||
export const MAX_LOCAL_LEGACY_ADOPTION_TASKS = 100_000;
|
||||
export const MAX_LOCAL_LEGACY_ADOPTION_TRIGGERS = 500_000;
|
||||
|
||||
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
|
||||
const UUID_V4_PATTERN =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
||||
const UUID_V7_PATTERN =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
||||
|
||||
export interface LocalLegacyAdoptionCandidate {
|
||||
readonly rowOrdinal: number;
|
||||
readonly sourceDigest: string;
|
||||
readonly task: Readonly<{
|
||||
taskId: string;
|
||||
name: string;
|
||||
kind: 'command';
|
||||
spec: TaskDefinitionSpec;
|
||||
labels: Readonly<Record<string, string>>;
|
||||
enabled: boolean;
|
||||
}>;
|
||||
readonly triggers: readonly Readonly<{
|
||||
triggerId: string;
|
||||
spec: TriggerSpec;
|
||||
enabled: boolean;
|
||||
}>[];
|
||||
}
|
||||
|
||||
export interface PublishLocalLegacyAdoptionCommand {
|
||||
readonly mutationId: string;
|
||||
readonly decisionId: string;
|
||||
readonly projectId: string;
|
||||
readonly profile: LocalSqliteProfile;
|
||||
readonly planDigest: string;
|
||||
readonly inventoryDigest: string;
|
||||
readonly decisionDigest: string;
|
||||
readonly receiptDigest: string;
|
||||
readonly authorizationFileDigest: string;
|
||||
readonly rowCount: number;
|
||||
readonly skippedCount: number;
|
||||
readonly subject: SecuritySubject;
|
||||
readonly fence: SecurityPolicyFence;
|
||||
readonly audit: SecurityAuditRecord;
|
||||
readonly candidates: Iterable<LocalLegacyAdoptionCandidate>;
|
||||
readonly confirmExternalAuthority: () => void | Promise<void>;
|
||||
readonly createdAtMs: number;
|
||||
}
|
||||
|
||||
export interface LocalLegacyAdoptionRecord {
|
||||
readonly mutationId: string;
|
||||
readonly decisionId: string;
|
||||
readonly projectId: string;
|
||||
readonly profile: LocalSqliteProfile;
|
||||
readonly planDigest: string;
|
||||
readonly inventoryDigest: string;
|
||||
readonly decisionDigest: string;
|
||||
readonly receiptDigest: string;
|
||||
readonly authorizationFileDigest: string;
|
||||
readonly publicationDigest: string;
|
||||
readonly rowCount: number;
|
||||
readonly adoptedTaskCount: number;
|
||||
readonly adoptedTriggerCount: number;
|
||||
readonly skippedCount: number;
|
||||
readonly auditEventId: string;
|
||||
readonly createdAtMs: number;
|
||||
}
|
||||
|
||||
export interface PublishLocalLegacyAdoptionResult {
|
||||
readonly status: 'inserted' | 'existing';
|
||||
readonly adoption: LocalLegacyAdoptionRecord;
|
||||
}
|
||||
|
||||
export class LocalLegacyAdoptionConflictError extends Error {
|
||||
readonly code = 'LOCAL_LEGACY_ADOPTION_CONFLICT';
|
||||
constructor() {
|
||||
super('Local legacy adoption conflicts with durable state');
|
||||
this.name = 'LocalLegacyAdoptionConflictError';
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalLegacyAdoptionAuthorizationFenceConflictError extends Error {
|
||||
readonly code = 'LOCAL_LEGACY_ADOPTION_AUTHORIZATION_FENCE_CONFLICT';
|
||||
constructor() {
|
||||
super('Local legacy adoption authorization fence changed');
|
||||
this.name = 'LocalLegacyAdoptionAuthorizationFenceConflictError';
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalLegacyAdoptionUnavailableError extends Error {
|
||||
readonly code = 'LOCAL_LEGACY_ADOPTION_UNAVAILABLE';
|
||||
constructor(readonly cause?: unknown) {
|
||||
super('Local legacy adoption storage is unavailable');
|
||||
this.name = 'LocalLegacyAdoptionUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
|
||||
function text(row: Row, key: string): string {
|
||||
const value = row[key];
|
||||
if (typeof value !== 'string')
|
||||
throw new LocalLegacyAdoptionUnavailableError();
|
||||
return value;
|
||||
}
|
||||
|
||||
function integer(row: Row, key: string): number {
|
||||
const value = row[key];
|
||||
if (!Number.isSafeInteger(value)) {
|
||||
throw new LocalLegacyAdoptionUnavailableError();
|
||||
}
|
||||
return value as number;
|
||||
}
|
||||
|
||||
function adoptionRecord(row: Row): LocalLegacyAdoptionRecord {
|
||||
const profile = text(row, 'profile');
|
||||
if (profile !== 'edge' && profile !== 'standalone') {
|
||||
throw new LocalLegacyAdoptionUnavailableError();
|
||||
}
|
||||
return Object.freeze({
|
||||
mutationId: text(row, 'mutationId'),
|
||||
decisionId: text(row, 'decisionId'),
|
||||
projectId: text(row, 'projectId'),
|
||||
profile,
|
||||
planDigest: text(row, 'planDigest'),
|
||||
inventoryDigest: text(row, 'inventoryDigest'),
|
||||
decisionDigest: text(row, 'decisionDigest'),
|
||||
receiptDigest: text(row, 'receiptDigest'),
|
||||
authorizationFileDigest: text(row, 'authorizationFileDigest'),
|
||||
publicationDigest: text(row, 'publicationDigest'),
|
||||
rowCount: integer(row, 'rowCount'),
|
||||
adoptedTaskCount: integer(row, 'adoptedTaskCount'),
|
||||
adoptedTriggerCount: integer(row, 'adoptedTriggerCount'),
|
||||
skippedCount: integer(row, 'skippedCount'),
|
||||
auditEventId: text(row, 'auditEventId'),
|
||||
createdAtMs: integer(row, 'createdAtMs'),
|
||||
});
|
||||
}
|
||||
|
||||
function deterministicMutationId(
|
||||
batchMutationId: string,
|
||||
identity: string,
|
||||
): string {
|
||||
const bytes = createHash('sha256')
|
||||
.update('qinglong3.legacy-adoption-mutation.v1\0')
|
||||
.update(batchMutationId)
|
||||
.update('\0')
|
||||
.update(identity)
|
||||
.digest()
|
||||
.subarray(0, 16);
|
||||
bytes[6] = ((bytes[6] as number) & 0x0f) | 0x40;
|
||||
bytes[8] = ((bytes[8] as number) & 0x3f) | 0x80;
|
||||
const hex = bytes.toString('hex');
|
||||
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(
|
||||
12,
|
||||
16,
|
||||
)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
||||
}
|
||||
|
||||
function exactReplay(
|
||||
existing: LocalLegacyAdoptionRecord,
|
||||
command: PublishLocalLegacyAdoptionCommand,
|
||||
): boolean {
|
||||
return (
|
||||
existing.mutationId === command.mutationId &&
|
||||
existing.decisionId === command.decisionId &&
|
||||
existing.projectId === command.projectId &&
|
||||
existing.profile === command.profile &&
|
||||
existing.planDigest === command.planDigest &&
|
||||
existing.inventoryDigest === command.inventoryDigest &&
|
||||
existing.decisionDigest === command.decisionDigest &&
|
||||
existing.receiptDigest === command.receiptDigest &&
|
||||
existing.authorizationFileDigest === command.authorizationFileDigest &&
|
||||
existing.rowCount === command.rowCount &&
|
||||
existing.skippedCount === command.skippedCount &&
|
||||
existing.auditEventId === command.audit.eventId &&
|
||||
existing.createdAtMs === command.createdAtMs
|
||||
);
|
||||
}
|
||||
|
||||
function assertCommand(command: PublishLocalLegacyAdoptionCommand): void {
|
||||
if (
|
||||
!command ||
|
||||
typeof command !== 'object' ||
|
||||
!UUID_V4_PATTERN.test(command.mutationId) ||
|
||||
!UUID_V7_PATTERN.test(command.decisionId) ||
|
||||
(command.profile !== 'edge' && command.profile !== 'standalone') ||
|
||||
![
|
||||
command.planDigest,
|
||||
command.inventoryDigest,
|
||||
command.decisionDigest,
|
||||
command.receiptDigest,
|
||||
command.authorizationFileDigest,
|
||||
].every((digest) => DIGEST_PATTERN.test(digest)) ||
|
||||
!Number.isSafeInteger(command.rowCount) ||
|
||||
command.rowCount < 0 ||
|
||||
command.rowCount > MAX_LOCAL_LEGACY_ADOPTION_TASKS ||
|
||||
!Number.isSafeInteger(command.skippedCount) ||
|
||||
command.skippedCount < 0 ||
|
||||
command.skippedCount > command.rowCount ||
|
||||
!Number.isSafeInteger(command.createdAtMs) ||
|
||||
command.createdAtMs < 0 ||
|
||||
!command.candidates ||
|
||||
typeof command.candidates[Symbol.iterator] !== 'function' ||
|
||||
typeof command.confirmExternalAuthority !== 'function'
|
||||
) {
|
||||
throw new LocalLegacyAdoptionConflictError();
|
||||
}
|
||||
}
|
||||
|
||||
function insertAudit(client: DatabaseSync, audit: SecurityAuditRecord): void {
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3SecurityAuditEvents" (
|
||||
"event_id", "request_id", "operation_id", "project_id",
|
||||
"subject_type", "subject_id", "authentication_id", "outcome",
|
||||
"reasons_json", "fence_project_version", "fence_binding_version",
|
||||
"occurred_at_ms"
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
const LEDGER_SELECT = `
|
||||
"mutation_id" AS "mutationId", "decision_id" AS "decisionId",
|
||||
"project_id" AS "projectId", "profile" AS "profile",
|
||||
"plan_digest" AS "planDigest", "inventory_digest" AS "inventoryDigest",
|
||||
"decision_digest" AS "decisionDigest", "receipt_digest" AS "receiptDigest",
|
||||
"authorization_file_digest" AS "authorizationFileDigest",
|
||||
"publication_digest" AS "publicationDigest", "row_count" AS "rowCount",
|
||||
"adopted_task_count" AS "adoptedTaskCount",
|
||||
"adopted_trigger_count" AS "adoptedTriggerCount",
|
||||
"skipped_count" AS "skippedCount", "audit_event_id" AS "auditEventId",
|
||||
"created_at_ms" AS "createdAtMs"`;
|
||||
|
||||
export class LocalSqliteLegacyAdoptionPublisher {
|
||||
constructor(private readonly authority: LocalSqliteOperationAuthority) {}
|
||||
|
||||
publish(
|
||||
input: PublishLocalLegacyAdoptionCommand,
|
||||
): Promise<PublishLocalLegacyAdoptionResult> {
|
||||
assertCommand(input);
|
||||
const subject = normalizeProjectPolicySubject(input.subject);
|
||||
const audit = normalizeSecurityAuditRecord(input.audit);
|
||||
const fence = input.fence;
|
||||
if (
|
||||
!fence ||
|
||||
!Number.isSafeInteger(fence.projectVersion) ||
|
||||
fence.projectVersion < 1 ||
|
||||
!Number.isSafeInteger(fence.bindingVersion) ||
|
||||
(fence.bindingVersion as number) < 1 ||
|
||||
audit.eventId !== input.mutationId ||
|
||||
audit.operationId !== 'task.adopt' ||
|
||||
audit.projectId !== input.projectId ||
|
||||
audit.subject?.type !== subject.type ||
|
||||
audit.subject?.id !== subject.id ||
|
||||
audit.outcome !== 'allowed' ||
|
||||
audit.fence?.projectVersion !== fence.projectVersion ||
|
||||
audit.fence.bindingVersion !== fence.bindingVersion ||
|
||||
audit.occurredAtMs !== input.createdAtMs
|
||||
) {
|
||||
return Promise.reject(new LocalLegacyAdoptionConflictError());
|
||||
}
|
||||
return import(
|
||||
'@qinglong/runtime-core/task-definition-execution-compiler'
|
||||
).then(({ compileLocalCommandTaskDefinition }) =>
|
||||
this.authority.enqueue(
|
||||
async () => {
|
||||
const client = this.authority.client;
|
||||
let began = false;
|
||||
try {
|
||||
client.exec('BEGIN IMMEDIATE');
|
||||
began = true;
|
||||
const replayRows = client
|
||||
.prepare(
|
||||
`SELECT ${LEDGER_SELECT}
|
||||
FROM "QingLong3LegacyAdoptions"
|
||||
WHERE "mutation_id" = ? OR "decision_id" = ? LIMIT 2`,
|
||||
)
|
||||
.all(input.mutationId, input.decisionId) as Row[];
|
||||
if (replayRows.length > 0) {
|
||||
if (replayRows.length !== 1) {
|
||||
throw new LocalLegacyAdoptionConflictError();
|
||||
}
|
||||
const existing = adoptionRecord(replayRows[0] as Row);
|
||||
if (!exactReplay(existing, input)) {
|
||||
throw new LocalLegacyAdoptionConflictError();
|
||||
}
|
||||
await input.confirmExternalAuthority();
|
||||
client.exec('COMMIT');
|
||||
began = false;
|
||||
return Object.freeze({
|
||||
status: 'existing' as const,
|
||||
adoption: existing,
|
||||
});
|
||||
}
|
||||
|
||||
const project = client
|
||||
.prepare(
|
||||
`SELECT "version", "status" FROM "QingLong3Projects"
|
||||
WHERE "id" = ? LIMIT 1`,
|
||||
)
|
||||
.get(input.projectId) as Row | undefined;
|
||||
if (
|
||||
!project ||
|
||||
integer(project, 'version') !== fence.projectVersion ||
|
||||
text(project, 'status') !== 'active'
|
||||
) {
|
||||
throw new LocalLegacyAdoptionAuthorizationFenceConflictError();
|
||||
}
|
||||
const binding = client
|
||||
.prepare(
|
||||
`SELECT "version", "state", "role"
|
||||
FROM "QingLong3ProjectRoleBindings"
|
||||
WHERE "project_id" = ? AND "subject_type" = ?
|
||||
AND "subject_id" = ?
|
||||
ORDER BY "version" DESC LIMIT 1`,
|
||||
)
|
||||
.get(input.projectId, subject.type, subject.id) as
|
||||
| Row
|
||||
| undefined;
|
||||
if (
|
||||
!binding ||
|
||||
integer(binding, 'version') !== fence.bindingVersion ||
|
||||
text(binding, 'state') !== 'active' ||
|
||||
!['owner', 'admin'].includes(text(binding, 'role'))
|
||||
) {
|
||||
throw new LocalLegacyAdoptionAuthorizationFenceConflictError();
|
||||
}
|
||||
|
||||
const taskRegistry = createBuiltInTaskSpecSemanticRegistry();
|
||||
const triggerRegistry = createBuiltInTriggerSpecSemanticRegistry();
|
||||
const dispatch = new LocalSqliteDispatchDefinitionStore(client);
|
||||
const publication = createHash('sha256')
|
||||
.update('qinglong3.legacy-adoption-publication.v1\0')
|
||||
.update(input.mutationId);
|
||||
let adoptedTaskCount = 0;
|
||||
let adoptedTriggerCount = 0;
|
||||
let previousRowOrdinal = 0;
|
||||
for (const candidate of input.candidates) {
|
||||
adoptedTaskCount += 1;
|
||||
if (
|
||||
adoptedTaskCount > MAX_LOCAL_LEGACY_ADOPTION_TASKS ||
|
||||
!candidate ||
|
||||
typeof candidate !== 'object' ||
|
||||
!Number.isSafeInteger(candidate.rowOrdinal) ||
|
||||
candidate.rowOrdinal <= previousRowOrdinal ||
|
||||
candidate.rowOrdinal > input.rowCount ||
|
||||
!DIGEST_PATTERN.test(candidate.sourceDigest) ||
|
||||
candidate.task.kind !== 'command'
|
||||
) {
|
||||
throw new LocalLegacyAdoptionConflictError();
|
||||
}
|
||||
previousRowOrdinal = candidate.rowOrdinal;
|
||||
const taskCommand = normalizeAppendTaskDefinitionRevisionCommand({
|
||||
projectId: input.projectId,
|
||||
taskId: candidate.task.taskId,
|
||||
expectedRevision: null,
|
||||
mutationId: deterministicMutationId(
|
||||
input.mutationId,
|
||||
`task:${candidate.rowOrdinal}`,
|
||||
),
|
||||
name: candidate.task.name,
|
||||
kind: candidate.task.kind,
|
||||
spec: candidate.task.spec,
|
||||
labels: candidate.task.labels,
|
||||
enabled: candidate.task.enabled,
|
||||
occurredAtMs: input.createdAtMs,
|
||||
});
|
||||
const task = createTaskDefinitionRecord(
|
||||
{
|
||||
...taskCommand,
|
||||
spec: taskRegistry.normalize({
|
||||
projectId: input.projectId,
|
||||
taskId: taskCommand.taskId,
|
||||
kind: taskCommand.kind,
|
||||
spec: taskCommand.spec,
|
||||
}),
|
||||
},
|
||||
input.createdAtMs,
|
||||
);
|
||||
if (
|
||||
task.spec.schema !== BUILT_IN_COMMAND_TASK_SPEC_SCHEMA ||
|
||||
client
|
||||
.prepare(
|
||||
`SELECT 1 FROM "QingLong3TaskDefinitions"
|
||||
WHERE "project_id" = ? AND "task_id" = ?`,
|
||||
)
|
||||
.get(task.projectId, task.taskId)
|
||||
) {
|
||||
throw new LocalLegacyAdoptionConflictError();
|
||||
}
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3TaskDefinitions" (
|
||||
"project_id", "task_id", "current_revision",
|
||||
"created_at_ms", "updated_at_ms"
|
||||
) VALUES (?, ?, 1, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
task.projectId,
|
||||
task.taskId,
|
||||
task.createdAtMs,
|
||||
task.updatedAtMs,
|
||||
);
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3TaskDefinitionRevisions" (
|
||||
"project_id", "task_id", "revision", "mutation_id",
|
||||
"name", "description", "kind", "spec_json", "labels_json",
|
||||
"enabled", "content_digest", "created_at_ms"
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
task.projectId,
|
||||
task.taskId,
|
||||
task.revision,
|
||||
task.mutationId,
|
||||
task.name,
|
||||
task.description ?? null,
|
||||
task.kind,
|
||||
JSON.stringify(task.spec),
|
||||
JSON.stringify(task.labels),
|
||||
task.enabled ? 1 : 0,
|
||||
task.contentDigest,
|
||||
task.updatedAtMs,
|
||||
);
|
||||
if (task.enabled) {
|
||||
dispatch.appendPlan(
|
||||
compileLocalCommandTaskDefinition(task, taskRegistry),
|
||||
);
|
||||
}
|
||||
publication
|
||||
.update('\0task\0')
|
||||
.update(String(candidate.rowOrdinal))
|
||||
.update('\0')
|
||||
.update(candidate.sourceDigest)
|
||||
.update('\0')
|
||||
.update(task.contentDigest);
|
||||
|
||||
for (const [
|
||||
triggerIndex,
|
||||
value,
|
||||
] of candidate.triggers.entries()) {
|
||||
adoptedTriggerCount += 1;
|
||||
if (
|
||||
adoptedTriggerCount > MAX_LOCAL_LEGACY_ADOPTION_TRIGGERS ||
|
||||
(value.enabled && !task.enabled)
|
||||
) {
|
||||
throw new LocalLegacyAdoptionConflictError();
|
||||
}
|
||||
const triggerCommand = normalizeAppendTriggerRevisionCommand({
|
||||
projectId: input.projectId,
|
||||
triggerId: value.triggerId,
|
||||
expectedRevision: null,
|
||||
mutationId: deterministicMutationId(
|
||||
input.mutationId,
|
||||
`trigger:${candidate.rowOrdinal}:${triggerIndex + 1}`,
|
||||
),
|
||||
taskId: task.taskId,
|
||||
taskRevision: task.revision,
|
||||
taskContentDigest: task.contentDigest,
|
||||
spec: value.spec,
|
||||
enabled: value.enabled,
|
||||
occurredAtMs: input.createdAtMs,
|
||||
});
|
||||
const trigger = createTriggerRecord(
|
||||
{
|
||||
...triggerCommand,
|
||||
spec: triggerRegistry.normalize({
|
||||
projectId: input.projectId,
|
||||
triggerId: triggerCommand.triggerId,
|
||||
taskId: task.taskId,
|
||||
taskRevision: task.revision,
|
||||
spec: triggerCommand.spec,
|
||||
}),
|
||||
},
|
||||
input.createdAtMs,
|
||||
);
|
||||
if (
|
||||
client
|
||||
.prepare(
|
||||
`SELECT 1 FROM "QingLong3Triggers"
|
||||
WHERE "project_id" = ? AND "trigger_id" = ?`,
|
||||
)
|
||||
.get(trigger.projectId, trigger.triggerId)
|
||||
) {
|
||||
throw new LocalLegacyAdoptionConflictError();
|
||||
}
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3Triggers" (
|
||||
"project_id", "trigger_id", "task_id", "current_revision",
|
||||
"created_at_ms", "updated_at_ms"
|
||||
) VALUES (?, ?, ?, 1, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
trigger.projectId,
|
||||
trigger.triggerId,
|
||||
trigger.taskId,
|
||||
trigger.createdAtMs,
|
||||
trigger.updatedAtMs,
|
||||
);
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3TriggerRevisions" (
|
||||
"project_id", "trigger_id", "revision", "mutation_id",
|
||||
"task_id", "task_revision", "task_content_digest",
|
||||
"spec_json", "enabled", "content_digest", "created_at_ms"
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
trigger.projectId,
|
||||
trigger.triggerId,
|
||||
trigger.revision,
|
||||
trigger.mutationId,
|
||||
trigger.taskId,
|
||||
trigger.taskRevision,
|
||||
trigger.taskContentDigest,
|
||||
JSON.stringify(trigger.spec),
|
||||
trigger.enabled ? 1 : 0,
|
||||
trigger.contentDigest,
|
||||
trigger.updatedAtMs,
|
||||
);
|
||||
const scheduleConfig = trigger.spec.config as Readonly<{
|
||||
expression?: unknown;
|
||||
timezone?: unknown;
|
||||
}>;
|
||||
if (
|
||||
trigger.spec.schema !== BUILT_IN_CRON_TRIGGER_SPEC_SCHEMA ||
|
||||
typeof scheduleConfig.expression !== 'string' ||
|
||||
typeof scheduleConfig.timezone !== 'string'
|
||||
) {
|
||||
throw new LocalLegacyAdoptionConflictError();
|
||||
}
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3LocalTriggerSchedules" (
|
||||
"project_id", "trigger_id", "trigger_revision",
|
||||
"next_fire_at_ms", "last_scheduled_at_ms",
|
||||
"state_version", "updated_at_ms"
|
||||
) VALUES (?, ?, ?, NULL, NULL, 0, ?)`,
|
||||
)
|
||||
.run(
|
||||
trigger.projectId,
|
||||
trigger.triggerId,
|
||||
trigger.revision,
|
||||
trigger.updatedAtMs,
|
||||
);
|
||||
publication.update('\0trigger\0').update(trigger.contentDigest);
|
||||
}
|
||||
}
|
||||
if (adoptedTaskCount + input.skippedCount !== input.rowCount) {
|
||||
throw new LocalLegacyAdoptionConflictError();
|
||||
}
|
||||
const publicationDigest = publication.digest('hex');
|
||||
insertAudit(client, audit);
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3LegacyAdoptions" (
|
||||
"mutation_id", "decision_id", "project_id", "profile",
|
||||
"plan_digest", "inventory_digest", "decision_digest",
|
||||
"receipt_digest", "authorization_file_digest",
|
||||
"publication_digest", "row_count", "adopted_task_count",
|
||||
"adopted_trigger_count", "skipped_count", "audit_event_id",
|
||||
"created_at_ms"
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
input.mutationId,
|
||||
input.decisionId,
|
||||
input.projectId,
|
||||
input.profile,
|
||||
input.planDigest,
|
||||
input.inventoryDigest,
|
||||
input.decisionDigest,
|
||||
input.receiptDigest,
|
||||
input.authorizationFileDigest,
|
||||
publicationDigest,
|
||||
input.rowCount,
|
||||
adoptedTaskCount,
|
||||
adoptedTriggerCount,
|
||||
input.skippedCount,
|
||||
audit.eventId,
|
||||
input.createdAtMs,
|
||||
);
|
||||
const stored = adoptionRecord(
|
||||
client
|
||||
.prepare(
|
||||
`SELECT ${LEDGER_SELECT} FROM "QingLong3LegacyAdoptions"
|
||||
WHERE "mutation_id" = ?`,
|
||||
)
|
||||
.get(input.mutationId) as Row,
|
||||
);
|
||||
await input.confirmExternalAuthority();
|
||||
client.exec('COMMIT');
|
||||
began = false;
|
||||
return Object.freeze({
|
||||
status: 'inserted' as const,
|
||||
adoption: stored,
|
||||
});
|
||||
} catch (error) {
|
||||
if (began && client.isTransaction) {
|
||||
try {
|
||||
client.exec('ROLLBACK');
|
||||
} catch {
|
||||
// Preserve the original failure.
|
||||
}
|
||||
}
|
||||
if (
|
||||
error instanceof LocalLegacyAdoptionConflictError ||
|
||||
error instanceof
|
||||
LocalLegacyAdoptionAuthorizationFenceConflictError ||
|
||||
error instanceof LocalLegacyAdoptionUnavailableError
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
if (
|
||||
error &&
|
||||
typeof error === 'object' &&
|
||||
'code' in error &&
|
||||
typeof error.code === 'string' &&
|
||||
error.code.startsWith('SQLITE_CONSTRAINT')
|
||||
) {
|
||||
throw new LocalLegacyAdoptionConflictError();
|
||||
}
|
||||
throw new LocalLegacyAdoptionUnavailableError(error);
|
||||
}
|
||||
},
|
||||
() => new LocalLegacyAdoptionUnavailableError(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface LocalSqliteAdoptionDatabase {
|
||||
readonly profile: LocalSqliteProfile;
|
||||
readonly readiness: LocalSqliteReadinessEvidence;
|
||||
readonly projectPolicy: ProjectPolicyRepository;
|
||||
readonly securityAudit: SecurityAuditSink;
|
||||
readonly publisher: LocalSqliteLegacyAdoptionPublisher;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
/** Short-lived reviewed adoption authority; long-lived Profile hosts must not import it. */
|
||||
export async function openLocalSqliteAdoptionDatabase(
|
||||
options: LocalSqliteDatabaseOptions,
|
||||
): Promise<LocalSqliteAdoptionDatabase> {
|
||||
assertLocalSqliteOptions(options);
|
||||
assertLocalSqlitePathBoundary(options.databasePath, false);
|
||||
const client = openLocalSqliteClient(options, false);
|
||||
try {
|
||||
const readiness = await auditLocalSqliteReadiness(client);
|
||||
const authority = new LocalSqliteOperationAuthority(client);
|
||||
const securityAuthority = new LocalSqliteSecurityAuthorityStore(authority);
|
||||
const projectPolicy: ProjectPolicyRepository = Object.freeze({
|
||||
resolve: (
|
||||
...[projectId, subject]: Parameters<ProjectPolicyRepository['resolve']>
|
||||
) => securityAuthority.resolve(projectId, subject),
|
||||
append: (...[command]: Parameters<ProjectPolicyRepository['append']>) =>
|
||||
securityAuthority.append(command),
|
||||
});
|
||||
let closePromise: Promise<void> | undefined;
|
||||
return Object.freeze({
|
||||
profile: options.profile,
|
||||
readiness,
|
||||
projectPolicy,
|
||||
securityAudit: securityAuthority,
|
||||
publisher: new LocalSqliteLegacyAdoptionPublisher(authority),
|
||||
close() {
|
||||
if (closePromise) return closePromise;
|
||||
closePromise = authority.close();
|
||||
return closePromise;
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
client.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import type { ApiCredentialRepository } from '@qinglong/runtime-core/api-credential';
|
||||
import type { ApprovalRequestRepository } from '@qinglong/runtime-core/approved-action';
|
||||
import type { ApprovalRequestDetailSource } from '@qinglong/runtime-core/approval-discovery';
|
||||
import type { LocalOwnerPepperRepository } from '@qinglong/runtime-core/local-owner-pepper';
|
||||
import type { ProjectPolicyRepository } from '@qinglong/runtime-core/project-policy';
|
||||
import type { SecurityAuditSink } from '@qinglong/runtime-core/security-audit';
|
||||
|
||||
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
|
||||
import { LocalSqliteOwnerPepperRepository } from '../local-owner/ownerPepperRepository';
|
||||
import {
|
||||
confirmLocalSqliteAuthenticatedUserCredentialFence,
|
||||
LocalSqliteAuthenticatedManagementFenceError,
|
||||
type LocalSqliteAuthenticatedUserCredentialFence,
|
||||
} from '../administration/packageManagement';
|
||||
import {
|
||||
auditLocalSqliteReadiness,
|
||||
type LocalSqliteReadinessEvidence,
|
||||
} from '../readiness/readiness';
|
||||
import { LocalSqliteApiCredentialRepository } from '../security/apiCredentialRepository';
|
||||
import { LocalSqliteProjectPolicyRepository } from '../security/projectPolicyRepository';
|
||||
import { LocalSqliteSecurityAuthorityStore } from '../security/securityAuthorityStore';
|
||||
import {
|
||||
assertLocalSqliteOptions,
|
||||
assertLocalSqlitePathBoundary,
|
||||
openLocalSqliteClient,
|
||||
type LocalSqliteDatabaseOptions,
|
||||
type LocalSqliteProfile,
|
||||
} from '../storage/config';
|
||||
import { LocalSqliteApprovalRequestRepository } from './approvalRequestRepository';
|
||||
import { LocalSqliteApprovalRequestSource } from './approvalRequestSource';
|
||||
|
||||
export interface LocalSqliteApprovalDecisionDatabase {
|
||||
readonly profile: LocalSqliteProfile;
|
||||
readonly readiness: LocalSqliteReadinessEvidence;
|
||||
readonly apiCredentials: ApiCredentialRepository;
|
||||
readonly ownerPepper: Pick<LocalOwnerPepperRepository, 'resolveKey'>;
|
||||
readonly projectPolicy: ProjectPolicyRepository;
|
||||
readonly approvals: Pick<ApprovalRequestRepository, 'findById' | 'decide'>;
|
||||
readonly approvalDetails: ApprovalRequestDetailSource;
|
||||
readonly securityAudit: SecurityAuditSink;
|
||||
activateUserCredentialFence(
|
||||
fence: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
|
||||
): void;
|
||||
confirmUserCredentialFence(): void;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
function sameCredentialFence(
|
||||
left: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
|
||||
right: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
|
||||
): boolean {
|
||||
return (
|
||||
left.credentialId === right.credentialId &&
|
||||
left.credentialVersion === right.credentialVersion &&
|
||||
left.pepperKeyId === right.pepperKeyId &&
|
||||
left.materialDigest === right.materialDigest &&
|
||||
left.subjectType === right.subjectType &&
|
||||
left.subjectId === right.subjectId &&
|
||||
left.secretDigest === right.secretDigest &&
|
||||
left.notBeforeAtMs === right.notBeforeAtMs &&
|
||||
left.expiresAtMs === right.expiresAtMs
|
||||
);
|
||||
}
|
||||
|
||||
/** Short-lived Owner authority; it never migrates schema or starts workers. */
|
||||
export async function openLocalSqliteApprovalDecisionDatabase(
|
||||
options: LocalSqliteDatabaseOptions,
|
||||
): Promise<LocalSqliteApprovalDecisionDatabase> {
|
||||
assertLocalSqliteOptions(options);
|
||||
assertLocalSqlitePathBoundary(options.databasePath, false);
|
||||
const client = openLocalSqliteClient(options, false);
|
||||
try {
|
||||
const readiness = await auditLocalSqliteReadiness(client);
|
||||
const authority = new LocalSqliteOperationAuthority(client);
|
||||
let activeFence:
|
||||
| Readonly<LocalSqliteAuthenticatedUserCredentialFence>
|
||||
| undefined;
|
||||
const confirmActiveFence = () => {
|
||||
if (!activeFence) {
|
||||
throw new LocalSqliteAuthenticatedManagementFenceError();
|
||||
}
|
||||
confirmLocalSqliteAuthenticatedUserCredentialFence(authority, activeFence);
|
||||
};
|
||||
const securityAuthority = new LocalSqliteSecurityAuthorityStore(authority);
|
||||
const approvals = new LocalSqliteApprovalRequestRepository(
|
||||
authority,
|
||||
confirmActiveFence,
|
||||
);
|
||||
let closePromise: Promise<void> | undefined;
|
||||
return Object.freeze({
|
||||
profile: options.profile,
|
||||
readiness,
|
||||
apiCredentials: new LocalSqliteApiCredentialRepository(authority),
|
||||
ownerPepper: new LocalSqliteOwnerPepperRepository(authority),
|
||||
projectPolicy: new LocalSqliteProjectPolicyRepository(authority),
|
||||
approvals,
|
||||
approvalDetails: new LocalSqliteApprovalRequestSource(authority),
|
||||
securityAudit: securityAuthority,
|
||||
activateUserCredentialFence(
|
||||
fence: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
|
||||
) {
|
||||
confirmLocalSqliteAuthenticatedUserCredentialFence(authority, fence);
|
||||
if (activeFence && !sameCredentialFence(activeFence, fence)) {
|
||||
throw new LocalSqliteAuthenticatedManagementFenceError();
|
||||
}
|
||||
activeFence = Object.freeze({ ...fence });
|
||||
},
|
||||
confirmUserCredentialFence: confirmActiveFence,
|
||||
close() {
|
||||
if (closePromise) return closePromise;
|
||||
closePromise = authority.close();
|
||||
return closePromise;
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (client.isOpen) client.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,622 @@
|
||||
import type { DatabaseSync } from 'node:sqlite';
|
||||
|
||||
import {
|
||||
ApprovalMutationConflictError,
|
||||
ApprovalPolicyFenceConflictError,
|
||||
ApprovalRequestNotFoundError,
|
||||
ApprovalRequestStateConflictError,
|
||||
ApprovalUnavailableError,
|
||||
approvalRequestDigest,
|
||||
approvedActionDispatchDigest,
|
||||
consumeApprovalRequest,
|
||||
decideApprovalRequest,
|
||||
normalizeApprovalRequestRecord,
|
||||
normalizeApprovedActionDispatchRecord,
|
||||
normalizeApprovedActionFence,
|
||||
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 {
|
||||
normalizeSecurityAuditRecord,
|
||||
type SecurityAuditRecord,
|
||||
} from '@qinglong/runtime-core/security-audit';
|
||||
import type {
|
||||
SecurityPolicyFence,
|
||||
SecuritySubject,
|
||||
} from '@qinglong/runtime-core/security';
|
||||
|
||||
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
|
||||
import {
|
||||
findLocalApprovedActionExecution,
|
||||
insertLocalApprovedActionExecutionBaseline,
|
||||
} from './approvedActionExecutionRepository';
|
||||
import { createApprovedActionExecution } from '@qinglong/runtime-core/approved-action-execution';
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
|
||||
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
|
||||
function text(row: Row, key: string): string {
|
||||
const value = row[key];
|
||||
if (typeof value !== 'string') throw new ApprovalUnavailableError();
|
||||
return value;
|
||||
}
|
||||
|
||||
function nullableText(row: Row, key: string): string | null {
|
||||
const value = row[key];
|
||||
if (value !== null && typeof value !== 'string') {
|
||||
throw new ApprovalUnavailableError();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function integer(row: Row, key: string): number {
|
||||
const value = row[key];
|
||||
if (!Number.isSafeInteger(value)) throw new ApprovalUnavailableError();
|
||||
return value as number;
|
||||
}
|
||||
|
||||
function nullableInteger(row: Row, key: string): number | null {
|
||||
const value = row[key];
|
||||
if (value !== null && !Number.isSafeInteger(value)) {
|
||||
throw new ApprovalUnavailableError();
|
||||
}
|
||||
return value as number | null;
|
||||
}
|
||||
|
||||
function same(left: unknown, right: unknown): boolean {
|
||||
return JSON.stringify(left) === JSON.stringify(right);
|
||||
}
|
||||
|
||||
function parseRequest(row: Row): Readonly<ApprovalRequestRecord> {
|
||||
try {
|
||||
const request = normalizeApprovalRequestRecord(
|
||||
JSON.parse(text(row, 'requestJson')) as ApprovalRequestRecord,
|
||||
);
|
||||
if (approvalRequestDigest(request) !== text(row, 'requestDigest')) {
|
||||
throw new ApprovalUnavailableError();
|
||||
}
|
||||
return request;
|
||||
} catch (error) {
|
||||
if (error instanceof ApprovalUnavailableError) throw error;
|
||||
throw new ApprovalUnavailableError();
|
||||
}
|
||||
}
|
||||
|
||||
function parseDispatch(row: Row): Readonly<ApprovedActionDispatchRecord> {
|
||||
try {
|
||||
const dispatch = normalizeApprovedActionDispatchRecord(
|
||||
JSON.parse(text(row, 'dispatchJson')) as ApprovedActionDispatchRecord,
|
||||
);
|
||||
if (
|
||||
approvedActionDispatchDigest(dispatch) !== text(row, 'dispatchDigest')
|
||||
) {
|
||||
throw new ApprovalUnavailableError();
|
||||
}
|
||||
return dispatch;
|
||||
} catch (error) {
|
||||
if (error instanceof ApprovalUnavailableError) throw error;
|
||||
throw new ApprovalUnavailableError();
|
||||
}
|
||||
}
|
||||
|
||||
function parseAudit(row: Row): Readonly<SecurityAuditRecord> {
|
||||
try {
|
||||
const subjectType = nullableText(row, 'subjectType');
|
||||
const subjectId = nullableText(row, 'subjectId');
|
||||
const fenceProjectVersion = nullableInteger(row, 'fenceProjectVersion');
|
||||
return normalizeSecurityAuditRecord({
|
||||
eventId: text(row, 'eventId'),
|
||||
requestId: text(row, 'requestId'),
|
||||
operationId: text(row, 'operationId'),
|
||||
projectId: nullableText(row, 'projectId'),
|
||||
subject:
|
||||
subjectType === null || subjectId === null
|
||||
? null
|
||||
: {
|
||||
type: subjectType as SecuritySubject['type'],
|
||||
id: subjectId,
|
||||
},
|
||||
authenticationId: nullableText(row, 'authenticationId'),
|
||||
outcome: text(row, 'outcome') as SecurityAuditRecord['outcome'],
|
||||
reasons: JSON.parse(text(row, 'reasonsJson')) as readonly string[],
|
||||
fence:
|
||||
fenceProjectVersion === null
|
||||
? null
|
||||
: {
|
||||
projectVersion: fenceProjectVersion,
|
||||
bindingVersion: nullableInteger(row, 'fenceBindingVersion'),
|
||||
},
|
||||
occurredAtMs: integer(row, 'occurredAtMs'),
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ApprovalUnavailableError) throw error;
|
||||
throw new ApprovalUnavailableError();
|
||||
}
|
||||
}
|
||||
|
||||
function storageError(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;
|
||||
}
|
||||
if (
|
||||
error &&
|
||||
typeof error === 'object' &&
|
||||
'code' in error &&
|
||||
typeof error.code === 'string' &&
|
||||
error.code.startsWith('SQLITE_CONSTRAINT')
|
||||
) {
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
export class LocalSqliteApprovalRequestRepository
|
||||
implements ApprovalRequestRepository
|
||||
{
|
||||
readonly #authority: LocalSqliteOperationAuthority;
|
||||
readonly #client: DatabaseSync;
|
||||
readonly #confirmMutation: () => void;
|
||||
|
||||
constructor(
|
||||
authority: LocalSqliteOperationAuthority | DatabaseSync,
|
||||
confirmMutation: () => void = () => undefined,
|
||||
) {
|
||||
if (typeof confirmMutation !== 'function') {
|
||||
throw new TypeError('Local Approval mutation guard is invalid');
|
||||
}
|
||||
this.#authority =
|
||||
authority instanceof LocalSqliteOperationAuthority
|
||||
? authority
|
||||
: new LocalSqliteOperationAuthority(authority);
|
||||
this.#client = this.#authority.client;
|
||||
this.#confirmMutation = confirmMutation;
|
||||
}
|
||||
|
||||
#enqueue<T>(work: () => T | Promise<T>): Promise<T> {
|
||||
return this.#authority.enqueue(
|
||||
async () => {
|
||||
try {
|
||||
return await work();
|
||||
} catch (error) {
|
||||
throw storageError(error);
|
||||
}
|
||||
},
|
||||
() => new ApprovalUnavailableError(),
|
||||
);
|
||||
}
|
||||
|
||||
#request(id: string): Readonly<ApprovalRequestRecord> | null {
|
||||
const row = this.#client
|
||||
.prepare(
|
||||
`SELECT "request_json" AS "requestJson",
|
||||
"request_digest" AS "requestDigest"
|
||||
FROM "QingLong3ApprovalRequests"
|
||||
WHERE "request_id" = ?`,
|
||||
)
|
||||
.get(id) as Row | undefined;
|
||||
return row ? parseRequest(row) : null;
|
||||
}
|
||||
|
||||
#dispatch(id: string): Readonly<ApprovedActionDispatchRecord> | null {
|
||||
const row = this.#client
|
||||
.prepare(
|
||||
`SELECT "dispatch_json" AS "dispatchJson",
|
||||
"dispatch_digest" AS "dispatchDigest"
|
||||
FROM "QingLong3ApprovedActionDispatches"
|
||||
WHERE "dispatch_id" = ?`,
|
||||
)
|
||||
.get(id) as Row | undefined;
|
||||
return row ? parseDispatch(row) : null;
|
||||
}
|
||||
|
||||
#audit(id: string): Readonly<SecurityAuditRecord> | null {
|
||||
const row = this.#client
|
||||
.prepare(
|
||||
`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_json" AS "reasonsJson",
|
||||
"fence_project_version" AS "fenceProjectVersion",
|
||||
"fence_binding_version" AS "fenceBindingVersion",
|
||||
"occurred_at_ms" AS "occurredAtMs"
|
||||
FROM "QingLong3SecurityAuditEvents"
|
||||
WHERE "event_id" = ?`,
|
||||
)
|
||||
.get(id) as Row | undefined;
|
||||
return row ? parseAudit(row) : null;
|
||||
}
|
||||
|
||||
#assertFence(
|
||||
projectId: string,
|
||||
subject: Readonly<SecuritySubject>,
|
||||
expectedValue: Readonly<SecurityPolicyFence>,
|
||||
): void {
|
||||
const expected = normalizeApprovedActionFence(expectedValue);
|
||||
const row = this.#client
|
||||
.prepare(
|
||||
`SELECT project."status" AS "status",
|
||||
project."version" AS "projectVersion",
|
||||
(
|
||||
SELECT max(binding."version")
|
||||
FROM "QingLong3ProjectRoleBindings" AS binding
|
||||
WHERE binding."project_id" = project."id"
|
||||
AND binding."subject_type" = ?
|
||||
AND binding."subject_id" = ?
|
||||
) AS "bindingVersion"
|
||||
FROM "QingLong3Projects" AS project
|
||||
WHERE project."id" = ?`,
|
||||
)
|
||||
.get(subject.type, subject.id, projectId) as Row | undefined;
|
||||
if (
|
||||
!row ||
|
||||
row.status !== 'active' ||
|
||||
integer(row, 'projectVersion') !== expected.projectVersion ||
|
||||
nullableInteger(row, 'bindingVersion') !== expected.bindingVersion
|
||||
) {
|
||||
throw new ApprovalPolicyFenceConflictError();
|
||||
}
|
||||
}
|
||||
|
||||
#insertAudit(value: SecurityAuditRecord): void {
|
||||
const audit = normalizeSecurityAuditRecord(value);
|
||||
this.#client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3SecurityAuditEvents" (
|
||||
"event_id", "request_id", "operation_id", "project_id",
|
||||
"subject_type", "subject_id", "authentication_id", "outcome",
|
||||
"reasons_json", "fence_project_version",
|
||||
"fence_binding_version", "occurred_at_ms"
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
#insertRequest(request: Readonly<ApprovalRequestRecord>): void {
|
||||
this.#client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3ApprovalRequests" (
|
||||
"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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
#updateRequest(
|
||||
request: Readonly<ApprovalRequestRecord>,
|
||||
expectedVersion: number,
|
||||
): void {
|
||||
const update = this.#client
|
||||
.prepare(
|
||||
`UPDATE "QingLong3ApprovalRequests"
|
||||
SET "version" = ?, "state" = ?, "decision_id" = ?,
|
||||
"consumption_id" = ?, "dispatch_id" = ?, "request_json" = ?,
|
||||
"request_digest" = ?, "updated_at_ms" = ?
|
||||
WHERE "request_id" = ? AND "version" = ?`,
|
||||
)
|
||||
.run(
|
||||
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 (update.changes !== 1) {
|
||||
throw new ApprovalRequestStateConflictError();
|
||||
}
|
||||
}
|
||||
|
||||
#insertDispatch(dispatch: Readonly<ApprovedActionDispatchRecord>): void {
|
||||
this.#client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3ApprovedActionDispatches" (
|
||||
"dispatch_id", "approval_request_id", "project_id", "action_type",
|
||||
"action_ref", "action_digest", "preview_digest", "dispatch_json",
|
||||
"dispatch_digest", "created_at_ms"
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
findById(id: string): Promise<Readonly<ApprovalRequestRecord> | null> {
|
||||
if (typeof id !== 'string' || !IDENTIFIER_PATTERN.test(id)) {
|
||||
throw new TypeError('Approval request lookup identity is invalid');
|
||||
}
|
||||
return this.#enqueue(() => this.#request(id));
|
||||
}
|
||||
|
||||
findDispatchById(
|
||||
id: string,
|
||||
): Promise<Readonly<ApprovedActionDispatchRecord> | null> {
|
||||
if (typeof id !== 'string' || !IDENTIFIER_PATTERN.test(id)) {
|
||||
throw new TypeError('Approved action dispatch identity is invalid');
|
||||
}
|
||||
return this.#enqueue(() => this.#dispatch(id));
|
||||
}
|
||||
|
||||
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.#enqueue(() => {
|
||||
this.#client.exec('BEGIN IMMEDIATE');
|
||||
try {
|
||||
this.#confirmMutation();
|
||||
const existing = this.#request(request.id);
|
||||
if (existing) {
|
||||
const storedAudit = this.#audit(audit.eventId);
|
||||
if (
|
||||
!same(existing, request) ||
|
||||
!storedAudit ||
|
||||
!same(storedAudit, audit)
|
||||
) {
|
||||
throw new ApprovalMutationConflictError();
|
||||
}
|
||||
this.#client.exec('COMMIT');
|
||||
return Object.freeze({ status: 'existing' as const, request });
|
||||
}
|
||||
this.#assertFence(
|
||||
request.projectId,
|
||||
request.requestedBy,
|
||||
request.requestFence,
|
||||
);
|
||||
this.#insertRequest(request);
|
||||
this.#insertAudit(audit);
|
||||
this.#client.exec('COMMIT');
|
||||
return Object.freeze({ status: 'created' as const, request });
|
||||
} catch (error) {
|
||||
if (this.#client.isTransaction) this.#client.exec('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
decide(
|
||||
command: DecideDurableApprovalRequestCommand,
|
||||
): Promise<DecideApprovalRequestResult> {
|
||||
const audit = normalizeSecurityAuditRecord(command.audit);
|
||||
return this.#enqueue(() => {
|
||||
this.#client.exec('BEGIN IMMEDIATE');
|
||||
try {
|
||||
this.#confirmMutation();
|
||||
const current = this.#request(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 = this.#audit(audit.eventId);
|
||||
if (!storedAudit || !same(storedAudit, audit)) {
|
||||
throw new ApprovalMutationConflictError();
|
||||
}
|
||||
this.#client.exec('COMMIT');
|
||||
return Object.freeze({
|
||||
status: 'existing' as const,
|
||||
request,
|
||||
});
|
||||
}
|
||||
this.#assertFence(
|
||||
request.projectId,
|
||||
command.principal.subject,
|
||||
command.authorizationFence,
|
||||
);
|
||||
this.#updateRequest(request, command.expectedVersion);
|
||||
this.#insertAudit(audit);
|
||||
this.#client.exec('COMMIT');
|
||||
return Object.freeze({ status: 'decided' as const, request });
|
||||
} catch (error) {
|
||||
if (this.#client.isTransaction) this.#client.exec('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
consume(
|
||||
command: ConsumeDurableApprovalRequestCommand,
|
||||
): Promise<ConsumeDurableApprovalRequestResult> {
|
||||
const audit = normalizeSecurityAuditRecord(command.audit);
|
||||
return this.#enqueue(() => {
|
||||
this.#client.exec('BEGIN IMMEDIATE');
|
||||
try {
|
||||
this.#confirmMutation();
|
||||
const current = this.#request(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 = this.#audit(audit.eventId);
|
||||
const dispatch = this.#dispatch(result.dispatch.id);
|
||||
const execution = findLocalApprovedActionExecution(
|
||||
this.#client,
|
||||
result.dispatch.id,
|
||||
);
|
||||
if (
|
||||
!storedAudit ||
|
||||
!same(storedAudit, audit) ||
|
||||
!dispatch ||
|
||||
!same(dispatch, result.dispatch) ||
|
||||
!execution ||
|
||||
!same(execution, createApprovedActionExecution(result.dispatch))
|
||||
) {
|
||||
throw new ApprovalMutationConflictError();
|
||||
}
|
||||
this.#client.exec('COMMIT');
|
||||
return Object.freeze({
|
||||
status: 'existing' as const,
|
||||
request: result.request,
|
||||
dispatch: result.dispatch,
|
||||
});
|
||||
}
|
||||
this.#assertFence(
|
||||
result.request.projectId,
|
||||
command.requestedBy,
|
||||
command.authorizationFence,
|
||||
);
|
||||
this.#insertDispatch(result.dispatch);
|
||||
insertLocalApprovedActionExecutionBaseline(
|
||||
this.#client,
|
||||
result.dispatch,
|
||||
);
|
||||
this.#updateRequest(result.request, command.expectedVersion);
|
||||
this.#insertAudit(audit);
|
||||
this.#client.exec('COMMIT');
|
||||
return Object.freeze({
|
||||
status: 'consumed' as const,
|
||||
request: result.request,
|
||||
dispatch: result.dispatch,
|
||||
});
|
||||
} catch (error) {
|
||||
if (this.#client.isTransaction) this.#client.exec('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
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 { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
|
||||
function text(row: Row, key: string): string {
|
||||
const value = row[key];
|
||||
if (typeof value !== 'string') throw new ApprovalUnavailableError();
|
||||
return value;
|
||||
}
|
||||
|
||||
function integer(row: Row, key: string): number {
|
||||
const value = row[key];
|
||||
if (!Number.isSafeInteger(value) || Number(value) < 0) {
|
||||
throw new ApprovalUnavailableError();
|
||||
}
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
function previewArtifact(
|
||||
row: Row,
|
||||
): Readonly<ToolInvocationPreviewArtifact> | null {
|
||||
if (row.previewArtifactId === null) return null;
|
||||
try {
|
||||
const value = normalizeToolInvocationPreviewArtifact(
|
||||
JSON.parse(text(row, 'previewArtifactJson')) as ToolInvocationPreviewArtifact,
|
||||
);
|
||||
if (
|
||||
value.artifactId !== text(row, 'previewArtifactId') ||
|
||||
value.projectId !== text(row, 'previewProjectId') ||
|
||||
value.actionRef !== text(row, 'previewActionRef') ||
|
||||
value.actionDigest !== text(row, 'previewActionDigest') ||
|
||||
value.previewDigest !== text(row, 'storedPreviewDigest') ||
|
||||
value.redactionContractDigest !== text(row, 'redactionContractDigest') ||
|
||||
value.artifactDigest !== text(row, 'previewArtifactDigest') ||
|
||||
value.byteLength !== integer(row, 'previewByteLength') ||
|
||||
value.sealedAtMs !== integer(row, 'previewSealedAtMs')
|
||||
) {
|
||||
throw new ApprovalUnavailableError();
|
||||
}
|
||||
return value;
|
||||
} catch (error) {
|
||||
if (error instanceof ApprovalUnavailableError) throw error;
|
||||
throw new ApprovalUnavailableError();
|
||||
}
|
||||
}
|
||||
|
||||
function request(row: Row): Readonly<ApprovalRequestRecord> {
|
||||
try {
|
||||
const value = normalizeApprovalRequestRecord(
|
||||
JSON.parse(text(row, 'requestJson')) as ApprovalRequestRecord,
|
||||
);
|
||||
if (
|
||||
approvalRequestDigest(value) !== text(row, 'requestDigest') ||
|
||||
approvalRequestUpdatedAtMs(value) !== integer(row, 'updatedAtMs')
|
||||
) {
|
||||
throw new ApprovalUnavailableError();
|
||||
}
|
||||
return value;
|
||||
} catch (error) {
|
||||
if (error instanceof ApprovalUnavailableError) throw error;
|
||||
throw new ApprovalUnavailableError();
|
||||
}
|
||||
}
|
||||
|
||||
/** Read-only, Project-scoped Approval history over the shared SQLite queue. */
|
||||
export class LocalSqliteApprovalRequestSource
|
||||
implements ApprovalRequestSource, ApprovalRequestDetailSource
|
||||
{
|
||||
constructor(private readonly authority: LocalSqliteOperationAuthority) {
|
||||
if (!(authority instanceof LocalSqliteOperationAuthority)) {
|
||||
throw new TypeError('Local Approval discovery authority is invalid');
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
return this.authority.enqueue(
|
||||
async () => {
|
||||
try {
|
||||
const rows = this.authority.client
|
||||
.prepare(
|
||||
`SELECT "request_json" AS "requestJson",
|
||||
"request_digest" AS "requestDigest",
|
||||
"updated_at_ms" AS "updatedAtMs"
|
||||
FROM "QingLong3ApprovalRequests"
|
||||
WHERE "project_id" = ?
|
||||
AND (
|
||||
? IS NULL OR "updated_at_ms" < ? OR
|
||||
("updated_at_ms" = ? AND "request_id" < ?)
|
||||
)
|
||||
ORDER BY "updated_at_ms" DESC, "request_id" DESC
|
||||
LIMIT ?`,
|
||||
)
|
||||
.all(
|
||||
options.projectId,
|
||||
after?.updatedAtMs ?? null,
|
||||
after?.updatedAtMs ?? null,
|
||||
after?.updatedAtMs ?? null,
|
||||
after?.requestId ?? '',
|
||||
options.limit + 1,
|
||||
) as Row[] | undefined;
|
||||
if (!Array.isArray(rows)) throw new ApprovalUnavailableError();
|
||||
const truncated = rows.length > options.limit;
|
||||
const requests = Object.freeze(
|
||||
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 new ApprovalUnavailableError();
|
||||
}
|
||||
},
|
||||
() => new ApprovalUnavailableError(),
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
return this.authority.enqueue(
|
||||
async () => {
|
||||
try {
|
||||
const rows = this.authority.client
|
||||
.prepare(
|
||||
`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 "QingLong3ApprovalRequests" a
|
||||
LEFT JOIN "ToolInvocationPreviewArtifacts" 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" = ? AND a."request_id" = ?
|
||||
LIMIT 2`,
|
||||
)
|
||||
.all(options.projectId, options.requestId) as Row[] | undefined;
|
||||
if (!Array.isArray(rows) || rows.length > 1) {
|
||||
throw new ApprovalUnavailableError();
|
||||
}
|
||||
if (!rows[0]) return null;
|
||||
const approval = request(rows[0]);
|
||||
const preview = previewArtifact(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 new ApprovalUnavailableError();
|
||||
}
|
||||
return Object.freeze({
|
||||
request: approval,
|
||||
preview: preview?.preview ?? null,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ApprovalUnavailableError) throw error;
|
||||
throw new ApprovalUnavailableError();
|
||||
}
|
||||
},
|
||||
() => new ApprovalUnavailableError(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
import type { DatabaseSync } from 'node:sqlite';
|
||||
|
||||
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 {
|
||||
normalizeApprovedActionDispatchRecord,
|
||||
type ApprovedActionDispatchRecord,
|
||||
} from '@qinglong/runtime-core/approved-action';
|
||||
|
||||
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
|
||||
function text(row: Row, key: string): string {
|
||||
const value = row[key];
|
||||
if (typeof value !== 'string') {
|
||||
throw new ApprovedActionExecutionUnavailableError();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseExecution(row: Row): Readonly<ApprovedActionExecutionRecord> {
|
||||
try {
|
||||
const execution = normalizeApprovedActionExecutionRecord(
|
||||
JSON.parse(text(row, 'executionJson')) as ApprovedActionExecutionRecord,
|
||||
);
|
||||
if (execution.executionDigest !== text(row, 'executionDigest')) {
|
||||
throw new ApprovedActionExecutionUnavailableError();
|
||||
}
|
||||
return execution;
|
||||
} catch (error) {
|
||||
if (error instanceof ApprovedActionExecutionUnavailableError) throw error;
|
||||
throw new ApprovedActionExecutionUnavailableError();
|
||||
}
|
||||
}
|
||||
|
||||
function parseDispatch(row: Row): Readonly<ApprovedActionDispatchRecord> {
|
||||
try {
|
||||
return normalizeApprovedActionDispatchRecord(
|
||||
JSON.parse(text(row, 'dispatchJson')) as ApprovedActionDispatchRecord,
|
||||
);
|
||||
} catch {
|
||||
throw new ApprovedActionExecutionUnavailableError();
|
||||
}
|
||||
}
|
||||
|
||||
function storageError(error: unknown): Error {
|
||||
if (
|
||||
error instanceof ApprovedActionExecutionFenceConflictError ||
|
||||
error instanceof ApprovedActionExecutionStateConflictError ||
|
||||
error instanceof ApprovedActionExecutionUnavailableError ||
|
||||
(error instanceof Error &&
|
||||
error.name.startsWith('ApprovedActionExecution'))
|
||||
) {
|
||||
return error;
|
||||
}
|
||||
return new ApprovedActionExecutionUnavailableError({
|
||||
cause: error instanceof Error ? error : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
const EXECUTION_COLUMNS = `
|
||||
"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"
|
||||
`;
|
||||
|
||||
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 function insertLocalApprovedActionExecutionBaseline(
|
||||
client: DatabaseSync,
|
||||
dispatchValue: ApprovedActionDispatchRecord,
|
||||
): Readonly<ApprovedActionExecutionRecord> {
|
||||
const execution = createApprovedActionExecution(dispatchValue);
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3ApprovedActionExecutions" (${EXECUTION_COLUMNS})
|
||||
VALUES (${new Array(21).fill('?').join(', ')})`,
|
||||
)
|
||||
.run(...executionValues(execution));
|
||||
return execution;
|
||||
}
|
||||
|
||||
export function findLocalApprovedActionExecution(
|
||||
client: DatabaseSync,
|
||||
dispatchId: string,
|
||||
): Readonly<ApprovedActionExecutionRecord> | null {
|
||||
const row = client
|
||||
.prepare(
|
||||
`SELECT "execution_json" AS "executionJson",
|
||||
"execution_digest" AS "executionDigest"
|
||||
FROM "QingLong3ApprovedActionExecutions"
|
||||
WHERE "dispatch_id" = ?`,
|
||||
)
|
||||
.get(dispatchId) as Row | undefined;
|
||||
return row ? parseExecution(row) : null;
|
||||
}
|
||||
|
||||
function snapshot(
|
||||
client: DatabaseSync,
|
||||
dispatchId: string,
|
||||
): Readonly<ApprovedActionExecutionSnapshot> | null {
|
||||
const row = client
|
||||
.prepare(
|
||||
`SELECT execution."execution_json" AS "executionJson",
|
||||
execution."execution_digest" AS "executionDigest",
|
||||
dispatch."dispatch_json" AS "dispatchJson"
|
||||
FROM "QingLong3ApprovedActionExecutions" AS execution
|
||||
JOIN "QingLong3ApprovedActionDispatches" AS dispatch
|
||||
ON dispatch."dispatch_id" = execution."dispatch_id"
|
||||
WHERE execution."dispatch_id" = ?`,
|
||||
)
|
||||
.get(dispatchId) as Row | undefined;
|
||||
if (!row) return null;
|
||||
return normalizeApprovedActionExecutionSnapshot({
|
||||
dispatch: parseDispatch(row),
|
||||
execution: parseExecution(row),
|
||||
});
|
||||
}
|
||||
|
||||
function replaceExecution(
|
||||
client: DatabaseSync,
|
||||
previous: Readonly<ApprovedActionExecutionRecord>,
|
||||
next: Readonly<ApprovedActionExecutionRecord>,
|
||||
): void {
|
||||
const result = client
|
||||
.prepare(
|
||||
`UPDATE "QingLong3ApprovedActionExecutions"
|
||||
SET "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" = ?
|
||||
WHERE "dispatch_id" = ? AND "version" = ?
|
||||
AND "execution_digest" = ?`,
|
||||
)
|
||||
.run(
|
||||
...executionValues(next).slice(1),
|
||||
previous.dispatchId,
|
||||
previous.version,
|
||||
previous.executionDigest,
|
||||
);
|
||||
if (result.changes !== 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 LocalSqliteApprovedActionExecutionRepository
|
||||
implements ApprovedActionExecutionRepository
|
||||
{
|
||||
readonly #authority: LocalSqliteOperationAuthority;
|
||||
readonly #client: DatabaseSync;
|
||||
|
||||
constructor(authority: LocalSqliteOperationAuthority | DatabaseSync) {
|
||||
this.#authority =
|
||||
authority instanceof LocalSqliteOperationAuthority
|
||||
? authority
|
||||
: new LocalSqliteOperationAuthority(authority);
|
||||
this.#client = this.#authority.client;
|
||||
}
|
||||
|
||||
#enqueue<T>(work: () => T | Promise<T>): Promise<T> {
|
||||
return this.#authority.enqueue(
|
||||
async () => {
|
||||
try {
|
||||
return await work();
|
||||
} catch (error) {
|
||||
throw storageError(error);
|
||||
}
|
||||
},
|
||||
() => new ApprovedActionExecutionUnavailableError(),
|
||||
);
|
||||
}
|
||||
|
||||
#transaction<T>(work: () => T): T {
|
||||
this.#client.exec('BEGIN IMMEDIATE');
|
||||
try {
|
||||
const result = work();
|
||||
this.#client.exec('COMMIT');
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (this.#client.isTransaction) this.#client.exec('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
findExecutionByDispatchId(
|
||||
dispatchId: string,
|
||||
): Promise<Readonly<ApprovedActionExecutionSnapshot> | null> {
|
||||
return this.#enqueue(() => snapshot(this.#client, dispatchId));
|
||||
}
|
||||
|
||||
listDueExecutions(
|
||||
query: ListDueApprovedActionExecutionsQuery,
|
||||
): Promise<ListDueApprovedActionExecutionsResult> {
|
||||
return this.#enqueue(() => {
|
||||
assertPageSize(query.limit);
|
||||
const handledActionTypes = actionTypes(query.actionTypes);
|
||||
const cursor = query.cursor
|
||||
? normalizeApprovedActionExecutionCursor(query.cursor)
|
||||
: undefined;
|
||||
if (!Number.isSafeInteger(query.nowMs) || query.nowMs < 0) {
|
||||
throw new ApprovedActionExecutionStateConflictError();
|
||||
}
|
||||
if (handledActionTypes.length === 0) {
|
||||
return Object.freeze({
|
||||
executions: Object.freeze([]),
|
||||
truncated: false,
|
||||
});
|
||||
}
|
||||
const actionTypePlaceholders = handledActionTypes.map(() => '?').join(',');
|
||||
const rows = this.#client
|
||||
.prepare(
|
||||
`SELECT execution."execution_json" AS "executionJson",
|
||||
execution."execution_digest" AS "executionDigest",
|
||||
dispatch."dispatch_json" AS "dispatchJson"
|
||||
FROM "QingLong3ApprovedActionExecutions" AS execution
|
||||
JOIN "QingLong3ApprovedActionDispatches" AS dispatch
|
||||
ON dispatch."dispatch_id" = execution."dispatch_id"
|
||||
WHERE execution."status" IN ('pending','leased','retry_wait')
|
||||
AND execution."eligible_at_ms" <= ?
|
||||
AND dispatch."action_type" IN (${actionTypePlaceholders})
|
||||
AND (
|
||||
? IS NULL OR execution."eligible_at_ms" > ? OR
|
||||
(execution."eligible_at_ms" = ? AND execution."dispatch_id" > ?)
|
||||
)
|
||||
ORDER BY execution."eligible_at_ms", execution."dispatch_id"
|
||||
LIMIT ?`,
|
||||
)
|
||||
.all(
|
||||
query.nowMs,
|
||||
...handledActionTypes,
|
||||
cursor?.dispatchId ?? null,
|
||||
cursor?.eligibleAtMs ?? 0,
|
||||
cursor?.eligibleAtMs ?? 0,
|
||||
cursor?.dispatchId ?? '',
|
||||
query.limit + 1,
|
||||
) as Row[];
|
||||
const truncated = rows.length > query.limit;
|
||||
const pageRows = truncated ? rows.slice(0, query.limit) : rows;
|
||||
const executions = pageRows.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,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
claimExecution(
|
||||
command: ClaimApprovedActionExecutionCommand,
|
||||
): Promise<ClaimApprovedActionExecutionResult> {
|
||||
return this.#enqueue(() =>
|
||||
this.#transaction(() => {
|
||||
const current = snapshot(this.#client, command.dispatchId);
|
||||
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,
|
||||
});
|
||||
replaceExecution(this.#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.#enqueue(() =>
|
||||
this.#transaction(() => {
|
||||
const current = snapshot(this.#client, dispatchId);
|
||||
if (!current) {
|
||||
throw new ApprovedActionExecutionStateConflictError();
|
||||
}
|
||||
const next = transition(current);
|
||||
replaceExecution(this.#client, current.execution, next);
|
||||
return Object.freeze({
|
||||
dispatch: current.dispatch,
|
||||
execution: next,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { DatabaseSync } from 'node:sqlite';
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
|
||||
function projectId(row: Row): string {
|
||||
const value = row.projectId;
|
||||
if (typeof value !== 'string') {
|
||||
throw new TypeError('Local instance authority Project is invalid');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function resolveLocalInstanceAuthorityProjectId(
|
||||
client: DatabaseSync,
|
||||
): string | null {
|
||||
const claimed = client
|
||||
.prepare(
|
||||
`SELECT "project_id" AS "projectId"
|
||||
FROM "QingLong3LocalOwnerBootstrapChallenges"
|
||||
WHERE "consumed_at_ms" IS NOT NULL
|
||||
ORDER BY "consumed_at_ms" ASC, "project_id" ASC, "version" ASC
|
||||
LIMIT 1`,
|
||||
)
|
||||
.get() as Row | undefined;
|
||||
if (claimed) return projectId(claimed);
|
||||
const fallback = client
|
||||
.prepare(
|
||||
`SELECT "id" AS "projectId"
|
||||
FROM "QingLong3Projects"
|
||||
WHERE "id" = 'default'
|
||||
LIMIT 1`,
|
||||
)
|
||||
.get() as Row | undefined;
|
||||
return fallback ? projectId(fallback) : null;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { DatabaseSync } from 'node:sqlite';
|
||||
|
||||
export const MAX_LOCAL_SQLITE_PENDING_OPERATIONS = 256;
|
||||
|
||||
/**
|
||||
* Owns the one synchronous SQLite connection, its bounded async admission
|
||||
* queue, and its close fence. Narrow repositories share this authority rather
|
||||
* than growing one public god repository or opening sibling connections.
|
||||
*/
|
||||
export class LocalSqliteOperationAuthority {
|
||||
private tail: Promise<void> = Promise.resolve();
|
||||
private pending = 0;
|
||||
private accepting = true;
|
||||
private closePromise?: Promise<void>;
|
||||
|
||||
constructor(readonly client: DatabaseSync) {}
|
||||
|
||||
enqueue<T>(
|
||||
work: () => Promise<T>,
|
||||
rejection: (reason: 'closed' | 'busy') => Error,
|
||||
): Promise<T> {
|
||||
if (!this.accepting) return Promise.reject(rejection('closed'));
|
||||
if (this.pending >= MAX_LOCAL_SQLITE_PENDING_OPERATIONS) {
|
||||
return Promise.reject(rejection('busy'));
|
||||
}
|
||||
this.pending += 1;
|
||||
const result = this.tail.then(work, work);
|
||||
this.tail = result.then(
|
||||
() => undefined,
|
||||
() => undefined,
|
||||
);
|
||||
return result.finally(() => {
|
||||
this.pending -= 1;
|
||||
});
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
if (this.closePromise) return this.closePromise;
|
||||
this.accepting = false;
|
||||
this.closePromise = this.tail.then(() => {
|
||||
if (this.client.isOpen) this.client.close();
|
||||
});
|
||||
return this.closePromise;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
export {
|
||||
LocalSqliteConfigurationError,
|
||||
auditLocalSqlitePath,
|
||||
migrateLocalSqlitePath,
|
||||
openLocalSqliteRuntimeDatabase,
|
||||
type LocalSqliteDatabaseOptions,
|
||||
type LocalSqliteMigrationResult,
|
||||
type LocalSqliteProfile,
|
||||
type LocalSqliteRuntimeDatabase,
|
||||
type LocalRunStartupRecoveryCandidate,
|
||||
type LocalRunStartupRecoveryPage,
|
||||
type LocalRunStartupRecoverySource,
|
||||
type LocalRunStartupRecoveryStatus,
|
||||
MAX_LOCAL_RUN_STARTUP_RECOVERY_CANDIDATES,
|
||||
} from './storage/database';
|
||||
|
||||
export {
|
||||
LOCAL_SQLITE_CONTRACT_NAME,
|
||||
LOCAL_SQLITE_CONTRACT_VERSION,
|
||||
LocalSqliteReadinessError,
|
||||
type LocalSqliteReadinessEvidence,
|
||||
} from './readiness/readiness';
|
||||
|
||||
export { LocalSqliteRunRepository } from './run/runRepository';
|
||||
export { LocalSqliteToolInvocationArtifactRepository } from './tool-execution/toolInvocationArtifactRepository';
|
||||
export { LocalSqliteApiCredentialRepository } from './security/apiCredentialRepository';
|
||||
export { LocalSqliteScheduleRepository } from './scheduling/scheduleRepository';
|
||||
export { LocalSqliteOwnerCredentialRecoveryRepository } from './local-owner/ownerCredentialRecoveryRepository';
|
||||
export { LocalSqliteOwnerDeliveryAcknowledgementGcRepository } from './local-owner/ownerDeliveryAcknowledgementGcRepository';
|
||||
export { LocalSqliteOwnerPepperMaterialGcRepository } from './local-owner/ownerPepperMaterialGcRepository';
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,671 @@
|
||||
import type { DatabaseSync } from 'node:sqlite';
|
||||
import {
|
||||
LocalOwnerCredentialRecoveryCredentialUnavailableError,
|
||||
LocalOwnerCredentialRecoveryInProgressError,
|
||||
LocalOwnerCredentialRecoveryMutationConflictError,
|
||||
LocalOwnerCredentialRecoveryNotAcknowledgedError,
|
||||
LocalOwnerCredentialRecoveryRepositoryUnavailableError,
|
||||
normalizeAcknowledgeLocalOwnerCredentialRecoveryCommand,
|
||||
normalizeCompleteLocalOwnerCredentialRecoveryCommand,
|
||||
normalizeIssueLocalOwnerCredentialRecoveryCommand,
|
||||
type AcknowledgeLocalOwnerCredentialRecoveryCommand,
|
||||
type CompleteLocalOwnerCredentialRecoveryCommand,
|
||||
type IssueLocalOwnerCredentialRecoveryCommand,
|
||||
type LocalOwnerCredentialRecoveryRecord,
|
||||
type LocalOwnerCredentialRecoveryRepository,
|
||||
type LocalOwnerCredentialRecoveryResult,
|
||||
} from '@qinglong/runtime-core/local-owner-credential-recovery';
|
||||
import {
|
||||
normalizeApiCredentialRecord,
|
||||
type ApiCredentialRecord,
|
||||
} from '@qinglong/runtime-core/api-credential';
|
||||
import type { SecurityAuditRecord } from '@qinglong/runtime-core/security-audit';
|
||||
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
|
||||
|
||||
type QueryRow = Record<string, unknown>;
|
||||
|
||||
const RECOVERY_SELECT = `
|
||||
SELECT recovery.*,
|
||||
replacement."state" AS "replacement_state",
|
||||
replacement."subject_type" AS "replacement_subject_type",
|
||||
replacement."subject_id" AS "replacement_subject_id",
|
||||
replacement."secret_digest" AS "replacement_secret_digest",
|
||||
replacement."created_at_ms" AS "replacement_created_at_ms",
|
||||
replacement."not_before_at_ms" AS "replacement_not_before_at_ms",
|
||||
replacement."expires_at_ms" AS "replacement_expires_at_ms",
|
||||
identity."status" AS "replacement_subject_status",
|
||||
pepper."pepper_key_id" AS "replacement_pepper_key_id"
|
||||
FROM "QingLong3LocalOwnerCredentialRecoveries" AS recovery
|
||||
JOIN "QingLong3ApiCredentials" AS replacement
|
||||
ON replacement."credential_id" = recovery."replacement_credential_id"
|
||||
AND replacement."version" = recovery."replacement_credential_version"
|
||||
JOIN "QingLong3IdentitySubjects" AS identity
|
||||
ON identity."subject_type" = replacement."subject_type"
|
||||
AND identity."subject_id" = replacement."subject_id"
|
||||
JOIN "QingLong3ApiCredentialPepperBindings" AS pepper
|
||||
ON pepper."credential_id" = replacement."credential_id"
|
||||
AND pepper."credential_version" = replacement."version"`;
|
||||
|
||||
const CREDENTIAL_SELECT = `
|
||||
SELECT credential."credential_id" AS "credential_credential_id",
|
||||
credential."version" AS "credential_credential_version",
|
||||
credential."state" AS "credential_state",
|
||||
credential."subject_type" AS "credential_subject_type",
|
||||
credential."subject_id" AS "credential_subject_id",
|
||||
credential."secret_digest" AS "credential_secret_digest",
|
||||
credential."created_at_ms" AS "credential_created_at_ms",
|
||||
credential."not_before_at_ms" AS "credential_not_before_at_ms",
|
||||
credential."expires_at_ms" AS "credential_expires_at_ms",
|
||||
identity."status" AS "credential_subject_status",
|
||||
pepper."pepper_key_id" AS "credential_pepper_key_id"
|
||||
FROM "QingLong3ApiCredentials" AS credential
|
||||
JOIN "QingLong3IdentitySubjects" AS identity
|
||||
ON identity."subject_type" = credential."subject_type"
|
||||
AND identity."subject_id" = credential."subject_id"
|
||||
JOIN "QingLong3ApiCredentialPepperBindings" AS pepper
|
||||
ON pepper."credential_id" = credential."credential_id"
|
||||
AND pepper."credential_version" = credential."version"`;
|
||||
|
||||
function string(row: QueryRow, name: string): string {
|
||||
const value = row[name];
|
||||
if (typeof value !== 'string') {
|
||||
throw new LocalOwnerCredentialRecoveryRepositoryUnavailableError();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function optionalString(row: QueryRow, name: string): string | undefined {
|
||||
const value = row[name];
|
||||
if (value === null || value === undefined) return undefined;
|
||||
return string(row, name);
|
||||
}
|
||||
|
||||
function integer(row: QueryRow, name: string): number {
|
||||
const value = row[name];
|
||||
if (typeof value !== 'number' || !Number.isSafeInteger(value)) {
|
||||
throw new LocalOwnerCredentialRecoveryRepositoryUnavailableError();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function optionalInteger(row: QueryRow, name: string): number | undefined {
|
||||
const value = row[name];
|
||||
if (value === null || value === undefined) return undefined;
|
||||
return integer(row, name);
|
||||
}
|
||||
|
||||
function credentialFromRow(
|
||||
row: QueryRow,
|
||||
prefix: 'credential' | 'replacement',
|
||||
): Readonly<ApiCredentialRecord> {
|
||||
try {
|
||||
return normalizeApiCredentialRecord({
|
||||
credentialId: string(row, `${prefix}_credential_id`),
|
||||
version: integer(row, `${prefix}_credential_version`),
|
||||
pepperKeyId: string(row, `${prefix}_pepper_key_id`),
|
||||
state: string(row, `${prefix}_state`) as ApiCredentialRecord['state'],
|
||||
subject: {
|
||||
type: string(
|
||||
row,
|
||||
`${prefix}_subject_type`,
|
||||
) as ApiCredentialRecord['subject']['type'],
|
||||
id: string(row, `${prefix}_subject_id`),
|
||||
},
|
||||
subjectStatus: string(
|
||||
row,
|
||||
`${prefix}_subject_status`,
|
||||
) as ApiCredentialRecord['subjectStatus'],
|
||||
secretDigest: string(row, `${prefix}_secret_digest`),
|
||||
createdAtMs: integer(row, `${prefix}_created_at_ms`),
|
||||
notBeforeAtMs: integer(row, `${prefix}_not_before_at_ms`),
|
||||
expiresAtMs: integer(row, `${prefix}_expires_at_ms`),
|
||||
});
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof LocalOwnerCredentialRecoveryRepositoryUnavailableError
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
throw new LocalOwnerCredentialRecoveryRepositoryUnavailableError();
|
||||
}
|
||||
}
|
||||
|
||||
function recoveryFromRow(
|
||||
row: QueryRow,
|
||||
): Readonly<LocalOwnerCredentialRecoveryRecord> {
|
||||
const state = string(row, 'state');
|
||||
if (state !== 'issued' && state !== 'acknowledged' && state !== 'completed') {
|
||||
throw new LocalOwnerCredentialRecoveryRepositoryUnavailableError();
|
||||
}
|
||||
const deliveryDigest = optionalString(row, 'delivery_digest');
|
||||
const acknowledgedAtMs = optionalInteger(row, 'acknowledged_at_ms');
|
||||
const completeMutationId = optionalString(row, 'complete_mutation_id');
|
||||
const completeRequestId = optionalString(row, 'complete_request_id');
|
||||
const revokedCredentialVersion = optionalInteger(
|
||||
row,
|
||||
'revoked_credential_version',
|
||||
);
|
||||
const completedAtMs = optionalInteger(row, 'completed_at_ms');
|
||||
if (
|
||||
(state === 'issued' &&
|
||||
(deliveryDigest !== undefined ||
|
||||
acknowledgedAtMs !== undefined ||
|
||||
completeMutationId !== undefined ||
|
||||
completeRequestId !== undefined ||
|
||||
revokedCredentialVersion !== undefined ||
|
||||
completedAtMs !== undefined)) ||
|
||||
(state === 'acknowledged' &&
|
||||
(deliveryDigest === undefined ||
|
||||
acknowledgedAtMs === undefined ||
|
||||
completeMutationId !== undefined ||
|
||||
completeRequestId !== undefined ||
|
||||
revokedCredentialVersion !== undefined ||
|
||||
completedAtMs !== undefined)) ||
|
||||
(state === 'completed' &&
|
||||
(deliveryDigest === undefined ||
|
||||
acknowledgedAtMs === undefined ||
|
||||
completeMutationId === undefined ||
|
||||
completeRequestId === undefined ||
|
||||
revokedCredentialVersion === undefined ||
|
||||
completedAtMs === undefined))
|
||||
) {
|
||||
throw new LocalOwnerCredentialRecoveryRepositoryUnavailableError();
|
||||
}
|
||||
return Object.freeze({
|
||||
issueMutationId: string(row, 'issue_mutation_id'),
|
||||
issueRequestId: string(row, 'issue_request_id'),
|
||||
subjectId: string(row, 'subject_id'),
|
||||
previousCredentialId: string(row, 'previous_credential_id'),
|
||||
previousCredentialVersion: integer(row, 'previous_credential_version'),
|
||||
replacementCredential: credentialFromRow(row, 'replacement'),
|
||||
state,
|
||||
issuedAtMs: integer(row, 'issued_at_ms'),
|
||||
...(deliveryDigest === undefined ? {} : { deliveryDigest }),
|
||||
...(acknowledgedAtMs === undefined ? {} : { acknowledgedAtMs }),
|
||||
...(completeMutationId === undefined ? {} : { completeMutationId }),
|
||||
...(completeRequestId === undefined ? {} : { completeRequestId }),
|
||||
...(revokedCredentialVersion === undefined
|
||||
? {}
|
||||
: { revokedCredentialVersion }),
|
||||
...(completedAtMs === undefined ? {} : { completedAtMs }),
|
||||
});
|
||||
}
|
||||
|
||||
function sameCredential(
|
||||
left: Readonly<ApiCredentialRecord>,
|
||||
right: Readonly<ApiCredentialRecord>,
|
||||
): boolean {
|
||||
return JSON.stringify(left) === JSON.stringify(right);
|
||||
}
|
||||
|
||||
function insertAudit(
|
||||
client: DatabaseSync,
|
||||
audit: Readonly<SecurityAuditRecord>,
|
||||
) {
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3SecurityAuditEvents" (
|
||||
event_id, request_id, operation_id, project_id, subject_type,
|
||||
subject_id, authentication_id, outcome, reasons_json,
|
||||
fence_project_version, fence_binding_version, occurred_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
function isDomainError(error: unknown): boolean {
|
||||
return (
|
||||
error instanceof LocalOwnerCredentialRecoveryMutationConflictError ||
|
||||
error instanceof LocalOwnerCredentialRecoveryInProgressError ||
|
||||
error instanceof LocalOwnerCredentialRecoveryNotAcknowledgedError ||
|
||||
error instanceof LocalOwnerCredentialRecoveryCredentialUnavailableError ||
|
||||
error instanceof LocalOwnerCredentialRecoveryRepositoryUnavailableError
|
||||
);
|
||||
}
|
||||
|
||||
export class LocalSqliteOwnerCredentialRecoveryRepository
|
||||
implements LocalOwnerCredentialRecoveryRepository
|
||||
{
|
||||
private readonly authority: LocalSqliteOperationAuthority;
|
||||
|
||||
constructor(authority: LocalSqliteOperationAuthority | DatabaseSync) {
|
||||
this.authority =
|
||||
authority instanceof LocalSqliteOperationAuthority
|
||||
? authority
|
||||
: new LocalSqliteOperationAuthority(authority);
|
||||
}
|
||||
|
||||
private resolveDirect(
|
||||
issueMutationId: string,
|
||||
): Readonly<LocalOwnerCredentialRecoveryRecord> | null {
|
||||
const rows = this.authority.client
|
||||
.prepare(
|
||||
`${RECOVERY_SELECT} WHERE recovery."issue_mutation_id" = ? LIMIT 2`,
|
||||
)
|
||||
.all(issueMutationId) as QueryRow[];
|
||||
if (rows.length > 1) {
|
||||
throw new LocalOwnerCredentialRecoveryRepositoryUnavailableError();
|
||||
}
|
||||
return rows[0] ? recoveryFromRow(rows[0]) : null;
|
||||
}
|
||||
|
||||
private currentCredentialDirect(
|
||||
credentialId: string,
|
||||
): Readonly<ApiCredentialRecord> | null {
|
||||
const row = this.authority.client
|
||||
.prepare(
|
||||
`${CREDENTIAL_SELECT}
|
||||
WHERE credential."credential_id" = ?
|
||||
ORDER BY credential."version" DESC LIMIT 1`,
|
||||
)
|
||||
.get(credentialId) as QueryRow | undefined;
|
||||
return row ? credentialFromRow(row, 'credential') : null;
|
||||
}
|
||||
|
||||
resolve(
|
||||
issueMutationId: string,
|
||||
): Promise<Readonly<LocalOwnerCredentialRecoveryRecord> | null> {
|
||||
return this.authority.enqueue(
|
||||
async () => {
|
||||
try {
|
||||
return this.resolveDirect(issueMutationId);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof
|
||||
LocalOwnerCredentialRecoveryRepositoryUnavailableError
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
throw new LocalOwnerCredentialRecoveryRepositoryUnavailableError();
|
||||
}
|
||||
},
|
||||
() => new LocalOwnerCredentialRecoveryRepositoryUnavailableError(),
|
||||
);
|
||||
}
|
||||
|
||||
issue(
|
||||
input: IssueLocalOwnerCredentialRecoveryCommand,
|
||||
): Promise<LocalOwnerCredentialRecoveryResult> {
|
||||
const command = normalizeIssueLocalOwnerCredentialRecoveryCommand(input);
|
||||
return this.authority.enqueue(
|
||||
async () => {
|
||||
const client = this.authority.client;
|
||||
client.exec('BEGIN IMMEDIATE');
|
||||
try {
|
||||
const replay = this.resolveDirect(command.mutationId);
|
||||
if (replay) {
|
||||
if (
|
||||
replay.issueRequestId !== command.requestId ||
|
||||
replay.previousCredentialId !== command.previousCredentialId ||
|
||||
replay.previousCredentialVersion !==
|
||||
command.expectedPreviousVersion ||
|
||||
!sameCredential(
|
||||
replay.replacementCredential,
|
||||
command.replacementCredential,
|
||||
)
|
||||
) {
|
||||
throw new LocalOwnerCredentialRecoveryMutationConflictError();
|
||||
}
|
||||
client.exec('COMMIT');
|
||||
return Object.freeze({
|
||||
status: 'existing' as const,
|
||||
recovery: replay,
|
||||
});
|
||||
}
|
||||
if (
|
||||
client
|
||||
.prepare(
|
||||
`SELECT 1 FROM "QingLong3LocalOwnerCredentialRecoveries"
|
||||
WHERE "subject_id" = ? AND "state" <> 'completed' LIMIT 1`,
|
||||
)
|
||||
.get(command.replacementCredential.subject.id)
|
||||
) {
|
||||
throw new LocalOwnerCredentialRecoveryInProgressError();
|
||||
}
|
||||
const previous = this.currentCredentialDirect(
|
||||
command.previousCredentialId,
|
||||
);
|
||||
if (
|
||||
!previous ||
|
||||
previous.version !== command.expectedPreviousVersion ||
|
||||
previous.state !== 'active' ||
|
||||
previous.subject.type !== 'user' ||
|
||||
previous.subject.id !== command.replacementCredential.subject.id ||
|
||||
previous.subjectStatus !== 'active' ||
|
||||
previous.notBeforeAtMs >
|
||||
command.replacementCredential.createdAtMs ||
|
||||
previous.expiresAtMs <= command.replacementCredential.createdAtMs
|
||||
) {
|
||||
throw new LocalOwnerCredentialRecoveryCredentialUnavailableError();
|
||||
}
|
||||
const activePepper = client
|
||||
.prepare(
|
||||
`SELECT key."pepper_key_id"
|
||||
FROM "QingLong3LocalOwnerPepperActivations" AS activation
|
||||
JOIN "QingLong3LocalOwnerPepperKeys" AS key
|
||||
ON key."pepper_key_id" = activation."active_pepper_key_id"
|
||||
WHERE key."state" = 'active'
|
||||
ORDER BY activation."generation" DESC LIMIT 1`,
|
||||
)
|
||||
.get() as QueryRow | undefined;
|
||||
if (
|
||||
!activePepper ||
|
||||
string(activePepper, 'pepper_key_id') !==
|
||||
command.replacementCredential.pepperKeyId
|
||||
) {
|
||||
throw new LocalOwnerCredentialRecoveryCredentialUnavailableError();
|
||||
}
|
||||
if (
|
||||
client
|
||||
.prepare(
|
||||
`SELECT 1 FROM "QingLong3ApiCredentials"
|
||||
WHERE "credential_id" = ? LIMIT 1`,
|
||||
)
|
||||
.get(command.replacementCredential.credentialId) ||
|
||||
client
|
||||
.prepare(
|
||||
`SELECT 1 FROM "QingLong3SecurityAuditEvents"
|
||||
WHERE "event_id" = ? LIMIT 1`,
|
||||
)
|
||||
.get(command.audit.eventId)
|
||||
) {
|
||||
throw new LocalOwnerCredentialRecoveryMutationConflictError();
|
||||
}
|
||||
const replacement = command.replacementCredential;
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3ApiCredentials" (
|
||||
credential_id, version, state, subject_type, subject_id,
|
||||
secret_digest, created_at_ms, not_before_at_ms, expires_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
replacement.credentialId,
|
||||
replacement.version,
|
||||
replacement.state,
|
||||
replacement.subject.type,
|
||||
replacement.subject.id,
|
||||
replacement.secretDigest,
|
||||
replacement.createdAtMs,
|
||||
replacement.notBeforeAtMs,
|
||||
replacement.expiresAtMs,
|
||||
);
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3ApiCredentialPepperBindings" (
|
||||
credential_id, credential_version, pepper_key_id
|
||||
) VALUES (?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
replacement.credentialId,
|
||||
replacement.version,
|
||||
replacement.pepperKeyId,
|
||||
);
|
||||
insertAudit(client, command.audit);
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3LocalOwnerCredentialRecoveries" (
|
||||
issue_mutation_id, issue_request_id, subject_type, subject_id,
|
||||
previous_credential_id, previous_credential_version,
|
||||
replacement_credential_id, replacement_credential_version,
|
||||
state, issued_at_ms, issue_audit_event_id
|
||||
) VALUES (?, ?, 'user', ?, ?, ?, ?, ?, 'issued', ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
command.mutationId,
|
||||
command.requestId,
|
||||
replacement.subject.id,
|
||||
command.previousCredentialId,
|
||||
command.expectedPreviousVersion,
|
||||
replacement.credentialId,
|
||||
replacement.version,
|
||||
replacement.createdAtMs,
|
||||
command.audit.eventId,
|
||||
);
|
||||
const recovery = this.resolveDirect(command.mutationId);
|
||||
if (!recovery) {
|
||||
throw new LocalOwnerCredentialRecoveryRepositoryUnavailableError();
|
||||
}
|
||||
client.exec('COMMIT');
|
||||
return Object.freeze({ status: 'inserted' as const, recovery });
|
||||
} catch (error) {
|
||||
if (client.isTransaction) client.exec('ROLLBACK');
|
||||
if (isDomainError(error)) throw error;
|
||||
throw new LocalOwnerCredentialRecoveryRepositoryUnavailableError();
|
||||
}
|
||||
},
|
||||
() => new LocalOwnerCredentialRecoveryRepositoryUnavailableError(),
|
||||
);
|
||||
}
|
||||
|
||||
acknowledge(
|
||||
input: AcknowledgeLocalOwnerCredentialRecoveryCommand,
|
||||
): Promise<LocalOwnerCredentialRecoveryResult> {
|
||||
const command =
|
||||
normalizeAcknowledgeLocalOwnerCredentialRecoveryCommand(input);
|
||||
return this.authority.enqueue(
|
||||
async () => {
|
||||
const client = this.authority.client;
|
||||
client.exec('BEGIN IMMEDIATE');
|
||||
try {
|
||||
const recovery = this.resolveDirect(command.issueMutationId);
|
||||
if (
|
||||
!recovery ||
|
||||
recovery.issueRequestId !== command.requestId ||
|
||||
recovery.replacementCredential.credentialId !==
|
||||
command.credentialId ||
|
||||
recovery.replacementCredential.secretDigest !==
|
||||
command.factDigest ||
|
||||
command.acknowledgedAtMs < recovery.issuedAtMs ||
|
||||
command.acknowledgedAtMs >=
|
||||
recovery.replacementCredential.expiresAtMs
|
||||
) {
|
||||
throw new LocalOwnerCredentialRecoveryMutationConflictError();
|
||||
}
|
||||
if (recovery.state !== 'issued') {
|
||||
if (
|
||||
recovery.deliveryDigest !== command.deliveryDigest ||
|
||||
recovery.acknowledgedAtMs !== command.acknowledgedAtMs
|
||||
) {
|
||||
throw new LocalOwnerCredentialRecoveryMutationConflictError();
|
||||
}
|
||||
client.exec('COMMIT');
|
||||
return Object.freeze({
|
||||
status: 'existing' as const,
|
||||
recovery,
|
||||
});
|
||||
}
|
||||
const changed = client
|
||||
.prepare(
|
||||
`UPDATE "QingLong3LocalOwnerCredentialRecoveries"
|
||||
SET state = 'acknowledged', delivery_digest = ?,
|
||||
acknowledged_at_ms = ?
|
||||
WHERE issue_mutation_id = ? AND state = 'issued'`,
|
||||
)
|
||||
.run(
|
||||
command.deliveryDigest,
|
||||
command.acknowledgedAtMs,
|
||||
command.issueMutationId,
|
||||
);
|
||||
if (changed.changes !== 1) {
|
||||
throw new LocalOwnerCredentialRecoveryMutationConflictError();
|
||||
}
|
||||
const acknowledged = this.resolveDirect(command.issueMutationId);
|
||||
if (!acknowledged || acknowledged.state !== 'acknowledged') {
|
||||
throw new LocalOwnerCredentialRecoveryRepositoryUnavailableError();
|
||||
}
|
||||
client.exec('COMMIT');
|
||||
return Object.freeze({
|
||||
status: 'inserted' as const,
|
||||
recovery: acknowledged,
|
||||
});
|
||||
} catch (error) {
|
||||
if (client.isTransaction) client.exec('ROLLBACK');
|
||||
if (isDomainError(error)) throw error;
|
||||
throw new LocalOwnerCredentialRecoveryRepositoryUnavailableError();
|
||||
}
|
||||
},
|
||||
() => new LocalOwnerCredentialRecoveryRepositoryUnavailableError(),
|
||||
);
|
||||
}
|
||||
|
||||
complete(
|
||||
input: CompleteLocalOwnerCredentialRecoveryCommand,
|
||||
): Promise<LocalOwnerCredentialRecoveryResult> {
|
||||
const command = normalizeCompleteLocalOwnerCredentialRecoveryCommand(input);
|
||||
return this.authority.enqueue(
|
||||
async () => {
|
||||
const client = this.authority.client;
|
||||
client.exec('BEGIN IMMEDIATE');
|
||||
try {
|
||||
const replayRow = client
|
||||
.prepare(
|
||||
`SELECT "issue_mutation_id"
|
||||
FROM "QingLong3LocalOwnerCredentialRecoveries"
|
||||
WHERE "complete_mutation_id" = ? LIMIT 1`,
|
||||
)
|
||||
.get(command.mutationId) as QueryRow | undefined;
|
||||
if (replayRow) {
|
||||
const replay = this.resolveDirect(
|
||||
string(replayRow, 'issue_mutation_id'),
|
||||
);
|
||||
const replayedRevocation = replay
|
||||
? this.currentCredentialDirect(replay.previousCredentialId)
|
||||
: null;
|
||||
if (
|
||||
!replay ||
|
||||
!replayedRevocation ||
|
||||
replay.issueMutationId !== command.issueMutationId ||
|
||||
replay.completeRequestId !== command.requestId ||
|
||||
replay.previousCredentialVersion !==
|
||||
command.expectedPreviousVersion ||
|
||||
replay.revokedCredentialVersion !==
|
||||
command.revokedCredential.version ||
|
||||
replay.completedAtMs !== command.revokedCredential.createdAtMs ||
|
||||
!sameCredential(replayedRevocation, command.revokedCredential)
|
||||
) {
|
||||
throw new LocalOwnerCredentialRecoveryMutationConflictError();
|
||||
}
|
||||
client.exec('COMMIT');
|
||||
return Object.freeze({
|
||||
status: 'existing' as const,
|
||||
recovery: replay,
|
||||
});
|
||||
}
|
||||
const recovery = this.resolveDirect(command.issueMutationId);
|
||||
if (!recovery) {
|
||||
throw new LocalOwnerCredentialRecoveryCredentialUnavailableError();
|
||||
}
|
||||
if (recovery.state === 'issued') {
|
||||
throw new LocalOwnerCredentialRecoveryNotAcknowledgedError();
|
||||
}
|
||||
if (recovery.state === 'completed') {
|
||||
throw new LocalOwnerCredentialRecoveryMutationConflictError();
|
||||
}
|
||||
const previous = this.currentCredentialDirect(
|
||||
recovery.previousCredentialId,
|
||||
);
|
||||
const revoked = command.revokedCredential;
|
||||
if (
|
||||
!previous ||
|
||||
previous.version !== command.expectedPreviousVersion ||
|
||||
previous.version !== recovery.previousCredentialVersion ||
|
||||
previous.state !== 'active' ||
|
||||
previous.subject.id !== recovery.subjectId ||
|
||||
revoked.credentialId !== previous.credentialId ||
|
||||
revoked.subject.type !== previous.subject.type ||
|
||||
revoked.subject.id !== previous.subject.id ||
|
||||
revoked.subjectStatus !== previous.subjectStatus ||
|
||||
revoked.pepperKeyId !== previous.pepperKeyId ||
|
||||
revoked.createdAtMs < (recovery.acknowledgedAtMs ?? 0) ||
|
||||
revoked.createdAtMs >= recovery.replacementCredential.expiresAtMs
|
||||
) {
|
||||
throw new LocalOwnerCredentialRecoveryCredentialUnavailableError();
|
||||
}
|
||||
if (
|
||||
client
|
||||
.prepare(
|
||||
`SELECT 1 FROM "QingLong3SecurityAuditEvents"
|
||||
WHERE "event_id" = ? LIMIT 1`,
|
||||
)
|
||||
.get(command.audit.eventId)
|
||||
) {
|
||||
throw new LocalOwnerCredentialRecoveryMutationConflictError();
|
||||
}
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3ApiCredentials" (
|
||||
credential_id, version, state, subject_type, subject_id,
|
||||
secret_digest, created_at_ms, not_before_at_ms, expires_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
revoked.credentialId,
|
||||
revoked.version,
|
||||
revoked.state,
|
||||
revoked.subject.type,
|
||||
revoked.subject.id,
|
||||
revoked.secretDigest,
|
||||
revoked.createdAtMs,
|
||||
revoked.notBeforeAtMs,
|
||||
revoked.expiresAtMs,
|
||||
);
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3ApiCredentialPepperBindings" (
|
||||
credential_id, credential_version, pepper_key_id
|
||||
) VALUES (?, ?, ?)`,
|
||||
)
|
||||
.run(revoked.credentialId, revoked.version, revoked.pepperKeyId);
|
||||
insertAudit(client, command.audit);
|
||||
const changed = client
|
||||
.prepare(
|
||||
`UPDATE "QingLong3LocalOwnerCredentialRecoveries"
|
||||
SET state = 'completed', complete_mutation_id = ?,
|
||||
complete_request_id = ?, revoked_credential_version = ?,
|
||||
completed_at_ms = ?, complete_audit_event_id = ?
|
||||
WHERE issue_mutation_id = ? AND state = 'acknowledged'`,
|
||||
)
|
||||
.run(
|
||||
command.mutationId,
|
||||
command.requestId,
|
||||
revoked.version,
|
||||
revoked.createdAtMs,
|
||||
command.audit.eventId,
|
||||
command.issueMutationId,
|
||||
);
|
||||
if (changed.changes !== 1) {
|
||||
throw new LocalOwnerCredentialRecoveryMutationConflictError();
|
||||
}
|
||||
const completed = this.resolveDirect(command.issueMutationId);
|
||||
if (!completed || completed.state !== 'completed') {
|
||||
throw new LocalOwnerCredentialRecoveryRepositoryUnavailableError();
|
||||
}
|
||||
client.exec('COMMIT');
|
||||
return Object.freeze({
|
||||
status: 'inserted' as const,
|
||||
recovery: completed,
|
||||
});
|
||||
} catch (error) {
|
||||
if (client.isTransaction) client.exec('ROLLBACK');
|
||||
if (isDomainError(error)) throw error;
|
||||
throw new LocalOwnerCredentialRecoveryRepositoryUnavailableError();
|
||||
}
|
||||
},
|
||||
() => new LocalOwnerCredentialRecoveryRepositoryUnavailableError(),
|
||||
);
|
||||
}
|
||||
}
|
||||
+508
@@ -0,0 +1,508 @@
|
||||
import type { DatabaseSync } from 'node:sqlite';
|
||||
import {
|
||||
localOwnerSecretDeliveryAcknowledgementSemanticDigest,
|
||||
normalizeLocalOwnerSecretDeliveryAcknowledgementRecord,
|
||||
type LocalOwnerSecretDeliveryAcknowledgementRecord,
|
||||
} from '@qinglong/runtime-core/local-owner-bootstrap';
|
||||
import {
|
||||
LocalOwnerDeliveryAcknowledgementGcMutationConflictError,
|
||||
LocalOwnerDeliveryAcknowledgementGcReferenceConflictError,
|
||||
LocalOwnerDeliveryAcknowledgementGcRepositoryUnavailableError,
|
||||
LocalOwnerDeliveryAcknowledgementGcRetentionPendingError,
|
||||
localOwnerDeliveryAcknowledgementGcRetentionPolicyDigest,
|
||||
normalizeCompactLocalOwnerDeliveryAcknowledgementCommand,
|
||||
type CompactLocalOwnerDeliveryAcknowledgementCommand,
|
||||
type LocalOwnerDeliveryAcknowledgementGcRecord,
|
||||
type LocalOwnerDeliveryAcknowledgementGcRepository,
|
||||
type LocalOwnerDeliveryAcknowledgementGcResult,
|
||||
} from '@qinglong/runtime-core/local-owner-delivery-acknowledgement-gc';
|
||||
import type { SecurityAuditRecord } from '@qinglong/runtime-core/security-audit';
|
||||
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
|
||||
|
||||
type QueryRow = Record<string, unknown>;
|
||||
|
||||
const GC_SELECT = `
|
||||
SELECT * FROM "QingLong3LocalOwnerDeliveryAcknowledgementGc"`;
|
||||
|
||||
function text(row: QueryRow, name: string): string {
|
||||
const value = row[name];
|
||||
if (typeof value !== 'string') {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcRepositoryUnavailableError();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function optionalText(row: QueryRow, name: string): string | undefined {
|
||||
const value = row[name];
|
||||
if (value === null || value === undefined) return undefined;
|
||||
return text(row, name);
|
||||
}
|
||||
|
||||
function integer(row: QueryRow, name: string): number {
|
||||
const value = row[name];
|
||||
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcRepositoryUnavailableError();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function safeRetentionTimestamp(base: number, duration: number): number {
|
||||
const result = base + duration;
|
||||
if (!Number.isSafeInteger(result) || result < 0) {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcRepositoryUnavailableError();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function acknowledgementFromRow(
|
||||
row: QueryRow,
|
||||
): Readonly<LocalOwnerSecretDeliveryAcknowledgementRecord> {
|
||||
try {
|
||||
const common = {
|
||||
mutationId: text(row, 'mutation_id'),
|
||||
requestId: text(row, 'request_id'),
|
||||
factDigest: text(row, 'fact_digest'),
|
||||
deliveryDigest: text(row, 'delivery_digest'),
|
||||
ttlMs: integer(row, 'ttl_ms'),
|
||||
acknowledgedAtMs: integer(row, 'acknowledged_at_ms'),
|
||||
};
|
||||
const kind = text(row, 'kind');
|
||||
return normalizeLocalOwnerSecretDeliveryAcknowledgementRecord(
|
||||
kind === 'credential'
|
||||
? {
|
||||
...common,
|
||||
kind,
|
||||
subjectId: text(row, 'subject_id'),
|
||||
credentialId: text(row, 'credential_id'),
|
||||
}
|
||||
: {
|
||||
...common,
|
||||
kind: kind as 'challenge',
|
||||
projectId: text(row, 'project_id'),
|
||||
challengeId: text(row, 'challenge_id'),
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof
|
||||
LocalOwnerDeliveryAcknowledgementGcRepositoryUnavailableError
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcRepositoryUnavailableError();
|
||||
}
|
||||
}
|
||||
|
||||
function gcRecord(
|
||||
row: QueryRow,
|
||||
): Readonly<LocalOwnerDeliveryAcknowledgementGcRecord> {
|
||||
const kind = text(row, 'acknowledgement_kind');
|
||||
const provisioningMutationId = optionalText(row, 'provisioning_mutation_id');
|
||||
const challengeMutationId = optionalText(row, 'challenge_mutation_id');
|
||||
const acknowledgementMutationId = text(row, 'acknowledgement_mutation_id');
|
||||
if (
|
||||
(kind !== 'credential' && kind !== 'challenge') ||
|
||||
(kind === 'credential' &&
|
||||
(provisioningMutationId !== acknowledgementMutationId ||
|
||||
challengeMutationId !== undefined)) ||
|
||||
(kind === 'challenge' &&
|
||||
(challengeMutationId !== acknowledgementMutationId ||
|
||||
provisioningMutationId !== undefined))
|
||||
) {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcRepositoryUnavailableError();
|
||||
}
|
||||
const retentionPolicy = Object.freeze({
|
||||
version: 1 as const,
|
||||
replayRetentionMs: integer(row, 'replay_retention_ms'),
|
||||
auditRetentionMs: integer(row, 'audit_retention_ms'),
|
||||
});
|
||||
const retentionPolicyDigest = text(row, 'retention_policy_digest');
|
||||
if (
|
||||
integer(row, 'retention_policy_version') !== 1 ||
|
||||
localOwnerDeliveryAcknowledgementGcRetentionPolicyDigest(
|
||||
retentionPolicy,
|
||||
) !== retentionPolicyDigest
|
||||
) {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcRepositoryUnavailableError();
|
||||
}
|
||||
return Object.freeze({
|
||||
mutationId: text(row, 'gc_mutation_id'),
|
||||
requestId: text(row, 'gc_request_id'),
|
||||
acknowledgementMutationId,
|
||||
acknowledgementKind: kind,
|
||||
deliveryDigest: text(row, 'delivery_digest'),
|
||||
acknowledgedAtMs: integer(row, 'acknowledged_at_ms'),
|
||||
acknowledgementSemanticDigest: text(row, 'acknowledgement_semantic_digest'),
|
||||
bridgeClearEvidenceDigest: text(row, 'bridge_clear_evidence_digest'),
|
||||
retentionPolicy,
|
||||
retentionPolicyDigest,
|
||||
retentionEligibleAtMs: integer(row, 'retention_eligible_at_ms'),
|
||||
compactedAtMs: integer(row, 'compacted_at_ms'),
|
||||
});
|
||||
}
|
||||
|
||||
function sameCompact(
|
||||
record: Readonly<LocalOwnerDeliveryAcknowledgementGcRecord>,
|
||||
command: Readonly<CompactLocalOwnerDeliveryAcknowledgementCommand>,
|
||||
): boolean {
|
||||
return (
|
||||
record.mutationId === command.mutationId &&
|
||||
record.requestId === command.requestId &&
|
||||
record.acknowledgementMutationId === command.acknowledgementMutationId &&
|
||||
record.acknowledgementKind === command.expectedKind &&
|
||||
record.deliveryDigest === command.expectedDeliveryDigest &&
|
||||
record.bridgeClearEvidenceDigest ===
|
||||
command.bridgeClearEvidence.evidenceDigest &&
|
||||
record.retentionPolicyDigest ===
|
||||
localOwnerDeliveryAcknowledgementGcRetentionPolicyDigest(
|
||||
command.retentionPolicy,
|
||||
) &&
|
||||
record.compactedAtMs === command.compactedAtMs
|
||||
);
|
||||
}
|
||||
|
||||
function insertAudit(
|
||||
client: DatabaseSync,
|
||||
audit: Readonly<SecurityAuditRecord>,
|
||||
): void {
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3SecurityAuditEvents" (
|
||||
event_id, request_id, operation_id, project_id, subject_type,
|
||||
subject_id, authentication_id, outcome, reasons_json,
|
||||
fence_project_version, fence_binding_version, occurred_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
function sourceRetentionEligibleAt(
|
||||
client: DatabaseSync,
|
||||
acknowledgement: Readonly<LocalOwnerSecretDeliveryAcknowledgementRecord>,
|
||||
command: Readonly<CompactLocalOwnerDeliveryAcknowledgementCommand>,
|
||||
): number {
|
||||
const replayEligibleAt = safeRetentionTimestamp(
|
||||
acknowledgement.acknowledgedAtMs,
|
||||
command.retentionPolicy.replayRetentionMs,
|
||||
);
|
||||
if (acknowledgement.kind === 'credential') {
|
||||
const source = client
|
||||
.prepare(
|
||||
`SELECT provisioning."request_id", provisioning."subject_id",
|
||||
provisioning."credential_id", credential."version",
|
||||
credential."secret_digest", credential."not_before_at_ms",
|
||||
credential."expires_at_ms", audit."occurred_at_ms"
|
||||
FROM "QingLong3LocalIdentityProvisionings" AS provisioning
|
||||
JOIN "QingLong3ApiCredentials" AS credential
|
||||
ON credential."credential_id" = provisioning."credential_id"
|
||||
AND credential."version" = provisioning."credential_version"
|
||||
JOIN "QingLong3SecurityAuditEvents" AS audit
|
||||
ON audit."event_id" = provisioning."audit_event_id"
|
||||
WHERE provisioning."mutation_id" = ?`,
|
||||
)
|
||||
.get(acknowledgement.mutationId) as QueryRow | undefined;
|
||||
if (
|
||||
!source ||
|
||||
text(source, 'request_id') !== acknowledgement.requestId ||
|
||||
text(source, 'subject_id') !== acknowledgement.subjectId ||
|
||||
text(source, 'credential_id') !== acknowledgement.credentialId ||
|
||||
text(source, 'secret_digest') !== acknowledgement.factDigest ||
|
||||
integer(source, 'expires_at_ms') - integer(source, 'not_before_at_ms') !==
|
||||
acknowledgement.ttlMs
|
||||
) {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcMutationConflictError();
|
||||
}
|
||||
const current = client
|
||||
.prepare(
|
||||
`SELECT "state", "expires_at_ms"
|
||||
FROM "QingLong3ApiCredentials"
|
||||
WHERE "credential_id" = ?
|
||||
ORDER BY "version" DESC LIMIT 1`,
|
||||
)
|
||||
.get(acknowledgement.credentialId) as QueryRow | undefined;
|
||||
if (
|
||||
!current ||
|
||||
(text(current, 'state') === 'active' &&
|
||||
integer(current, 'expires_at_ms') > command.compactedAtMs)
|
||||
) {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcReferenceConflictError();
|
||||
}
|
||||
if (
|
||||
client
|
||||
.prepare(
|
||||
`SELECT 1
|
||||
FROM "QingLong3LocalOwnerCredentialRecoveries"
|
||||
WHERE "state" <> 'completed'
|
||||
AND (("previous_credential_id" = ? AND "previous_credential_version" = ?)
|
||||
OR ("replacement_credential_id" = ? AND "replacement_credential_version" = ?))
|
||||
LIMIT 1`,
|
||||
)
|
||||
.get(
|
||||
acknowledgement.credentialId,
|
||||
integer(source, 'version'),
|
||||
acknowledgement.credentialId,
|
||||
integer(source, 'version'),
|
||||
)
|
||||
) {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcReferenceConflictError();
|
||||
}
|
||||
return Math.max(
|
||||
replayEligibleAt,
|
||||
integer(source, 'expires_at_ms'),
|
||||
safeRetentionTimestamp(
|
||||
integer(source, 'occurred_at_ms'),
|
||||
command.retentionPolicy.auditRetentionMs,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const source = client
|
||||
.prepare(
|
||||
`SELECT challenge."issue_request_id", challenge."project_id",
|
||||
challenge."challenge_id", challenge."token_digest",
|
||||
challenge."issued_at_ms", challenge."expires_at_ms",
|
||||
challenge."consumed_at_ms",
|
||||
issue_audit."occurred_at_ms" AS "issue_audit_at_ms",
|
||||
claim_audit."occurred_at_ms" AS "claim_audit_at_ms"
|
||||
FROM "QingLong3LocalOwnerBootstrapChallenges" AS challenge
|
||||
JOIN "QingLong3SecurityAuditEvents" AS issue_audit
|
||||
ON issue_audit."event_id" = challenge."issue_audit_event_id"
|
||||
LEFT JOIN "QingLong3SecurityAuditEvents" AS claim_audit
|
||||
ON claim_audit."event_id" = challenge."claim_audit_event_id"
|
||||
WHERE challenge."issue_mutation_id" = ?`,
|
||||
)
|
||||
.get(acknowledgement.mutationId) as QueryRow | undefined;
|
||||
if (
|
||||
!source ||
|
||||
text(source, 'issue_request_id') !== acknowledgement.requestId ||
|
||||
text(source, 'project_id') !== acknowledgement.projectId ||
|
||||
text(source, 'challenge_id') !== acknowledgement.challengeId ||
|
||||
text(source, 'token_digest') !== acknowledgement.factDigest ||
|
||||
integer(source, 'expires_at_ms') - integer(source, 'issued_at_ms') !==
|
||||
acknowledgement.ttlMs
|
||||
) {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcMutationConflictError();
|
||||
}
|
||||
if (
|
||||
source.consumed_at_ms === null &&
|
||||
integer(source, 'expires_at_ms') > command.compactedAtMs
|
||||
) {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcReferenceConflictError();
|
||||
}
|
||||
const claimAuditAtMs =
|
||||
source.claim_audit_at_ms === null
|
||||
? undefined
|
||||
: integer(source, 'claim_audit_at_ms');
|
||||
return Math.max(
|
||||
replayEligibleAt,
|
||||
integer(source, 'expires_at_ms'),
|
||||
safeRetentionTimestamp(
|
||||
integer(source, 'issue_audit_at_ms'),
|
||||
command.retentionPolicy.auditRetentionMs,
|
||||
),
|
||||
...(claimAuditAtMs === undefined
|
||||
? []
|
||||
: [
|
||||
safeRetentionTimestamp(
|
||||
claimAuditAtMs,
|
||||
command.retentionPolicy.auditRetentionMs,
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
function isDomainError(error: unknown): boolean {
|
||||
return (
|
||||
error instanceof LocalOwnerDeliveryAcknowledgementGcMutationConflictError ||
|
||||
error instanceof
|
||||
LocalOwnerDeliveryAcknowledgementGcReferenceConflictError ||
|
||||
error instanceof LocalOwnerDeliveryAcknowledgementGcRetentionPendingError ||
|
||||
error instanceof
|
||||
LocalOwnerDeliveryAcknowledgementGcRepositoryUnavailableError
|
||||
);
|
||||
}
|
||||
|
||||
export class LocalSqliteOwnerDeliveryAcknowledgementGcRepository
|
||||
implements LocalOwnerDeliveryAcknowledgementGcRepository
|
||||
{
|
||||
private readonly authority: LocalSqliteOperationAuthority;
|
||||
|
||||
constructor(authority: LocalSqliteOperationAuthority | DatabaseSync) {
|
||||
this.authority =
|
||||
authority instanceof LocalSqliteOperationAuthority
|
||||
? authority
|
||||
: new LocalSqliteOperationAuthority(authority);
|
||||
}
|
||||
|
||||
resolveByAcknowledgement(
|
||||
acknowledgementMutationId: string,
|
||||
): Promise<Readonly<LocalOwnerDeliveryAcknowledgementGcRecord> | null> {
|
||||
if (typeof acknowledgementMutationId !== 'string') {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcRepositoryUnavailableError();
|
||||
}
|
||||
return this.authority.enqueue(
|
||||
async () => {
|
||||
try {
|
||||
const row = this.authority.client
|
||||
.prepare(`${GC_SELECT} WHERE "acknowledgement_mutation_id" = ?`)
|
||||
.get(acknowledgementMutationId) as QueryRow | undefined;
|
||||
return row ? gcRecord(row) : null;
|
||||
} catch (error) {
|
||||
if (isDomainError(error)) throw error;
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcRepositoryUnavailableError();
|
||||
}
|
||||
},
|
||||
() => new LocalOwnerDeliveryAcknowledgementGcRepositoryUnavailableError(),
|
||||
);
|
||||
}
|
||||
|
||||
compact(
|
||||
candidate: CompactLocalOwnerDeliveryAcknowledgementCommand,
|
||||
): Promise<Readonly<LocalOwnerDeliveryAcknowledgementGcResult>> {
|
||||
const command =
|
||||
normalizeCompactLocalOwnerDeliveryAcknowledgementCommand(candidate);
|
||||
return this.authority.enqueue(
|
||||
async () => {
|
||||
const client = this.authority.client;
|
||||
try {
|
||||
client.exec('BEGIN IMMEDIATE');
|
||||
const replay = client
|
||||
.prepare(`${GC_SELECT} WHERE "gc_mutation_id" = ?`)
|
||||
.get(command.mutationId) as QueryRow | undefined;
|
||||
if (replay) {
|
||||
const record = gcRecord(replay);
|
||||
if (!sameCompact(record, command)) {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcMutationConflictError();
|
||||
}
|
||||
client.exec('COMMIT');
|
||||
return Object.freeze({ status: 'existing' as const, record });
|
||||
}
|
||||
if (
|
||||
client
|
||||
.prepare(`${GC_SELECT} WHERE "acknowledgement_mutation_id" = ?`)
|
||||
.get(command.acknowledgementMutationId)
|
||||
) {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcMutationConflictError();
|
||||
}
|
||||
const row = client
|
||||
.prepare(
|
||||
`SELECT * FROM "QingLong3LocalOwnerDeliveryAcknowledgements"
|
||||
WHERE "mutation_id" = ?`,
|
||||
)
|
||||
.get(command.acknowledgementMutationId) as QueryRow | undefined;
|
||||
if (!row) {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcMutationConflictError();
|
||||
}
|
||||
const acknowledgement = acknowledgementFromRow(row);
|
||||
if (
|
||||
acknowledgement.kind !== command.expectedKind ||
|
||||
acknowledgement.deliveryDigest !== command.expectedDeliveryDigest
|
||||
) {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcMutationConflictError();
|
||||
}
|
||||
const eligibleAtMs = sourceRetentionEligibleAt(
|
||||
client,
|
||||
acknowledgement,
|
||||
command,
|
||||
);
|
||||
if (command.compactedAtMs < eligibleAtMs) {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcRetentionPendingError(
|
||||
eligibleAtMs,
|
||||
);
|
||||
}
|
||||
if (
|
||||
client
|
||||
.prepare(
|
||||
`SELECT 1 FROM "QingLong3SecurityAuditEvents"
|
||||
WHERE "event_id" = ?`,
|
||||
)
|
||||
.get(command.audit.eventId)
|
||||
) {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcMutationConflictError();
|
||||
}
|
||||
const semanticDigest =
|
||||
localOwnerSecretDeliveryAcknowledgementSemanticDigest(
|
||||
acknowledgement,
|
||||
);
|
||||
const policyDigest =
|
||||
localOwnerDeliveryAcknowledgementGcRetentionPolicyDigest(
|
||||
command.retentionPolicy,
|
||||
);
|
||||
insertAudit(client, command.audit);
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3LocalOwnerDeliveryAcknowledgementGc" (
|
||||
gc_mutation_id, gc_request_id,
|
||||
acknowledgement_mutation_id, acknowledgement_kind,
|
||||
delivery_digest, acknowledged_at_ms,
|
||||
acknowledgement_semantic_digest,
|
||||
bridge_clear_evidence_digest, retention_policy_version,
|
||||
replay_retention_ms, audit_retention_ms,
|
||||
retention_policy_digest, retention_eligible_at_ms,
|
||||
compacted_at_ms, audit_event_id,
|
||||
provisioning_mutation_id, challenge_mutation_id
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
command.mutationId,
|
||||
command.requestId,
|
||||
acknowledgement.mutationId,
|
||||
acknowledgement.kind,
|
||||
acknowledgement.deliveryDigest,
|
||||
acknowledgement.acknowledgedAtMs,
|
||||
semanticDigest,
|
||||
command.bridgeClearEvidence.evidenceDigest,
|
||||
command.retentionPolicy.replayRetentionMs,
|
||||
command.retentionPolicy.auditRetentionMs,
|
||||
policyDigest,
|
||||
eligibleAtMs,
|
||||
command.compactedAtMs,
|
||||
command.audit.eventId,
|
||||
acknowledgement.kind === 'credential'
|
||||
? acknowledgement.mutationId
|
||||
: null,
|
||||
acknowledgement.kind === 'challenge'
|
||||
? acknowledgement.mutationId
|
||||
: null,
|
||||
);
|
||||
const deleted = client
|
||||
.prepare(
|
||||
`DELETE FROM "QingLong3LocalOwnerDeliveryAcknowledgements"
|
||||
WHERE "mutation_id" = ?`,
|
||||
)
|
||||
.run(acknowledgement.mutationId);
|
||||
if (deleted.changes !== 1) {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcMutationConflictError();
|
||||
}
|
||||
const inserted = client
|
||||
.prepare(`${GC_SELECT} WHERE "gc_mutation_id" = ?`)
|
||||
.get(command.mutationId) as QueryRow | undefined;
|
||||
if (!inserted) {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcRepositoryUnavailableError();
|
||||
}
|
||||
const record = gcRecord(inserted);
|
||||
client.exec('COMMIT');
|
||||
return Object.freeze({ status: 'inserted' as const, record });
|
||||
} catch (error) {
|
||||
if (client.isTransaction) client.exec('ROLLBACK');
|
||||
if (isDomainError(error)) throw error;
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcRepositoryUnavailableError();
|
||||
}
|
||||
},
|
||||
() => new LocalOwnerDeliveryAcknowledgementGcRepositoryUnavailableError(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,607 @@
|
||||
import type { DatabaseSync } from 'node:sqlite';
|
||||
import {
|
||||
LocalOwnerPepperMaterialGcInProgressError,
|
||||
LocalOwnerPepperMaterialGcMutationConflictError,
|
||||
LocalOwnerPepperMaterialGcReferenceConflictError,
|
||||
LocalOwnerPepperMaterialGcRepositoryUnavailableError,
|
||||
LocalOwnerPepperMaterialGcRetentionPendingError,
|
||||
localOwnerPepperMaterialGcRetentionPolicyDigest,
|
||||
normalizeCompleteLocalOwnerPepperMaterialGcCommand,
|
||||
normalizePrepareLocalOwnerPepperMaterialGcCommand,
|
||||
type CompleteLocalOwnerPepperMaterialGcCommand,
|
||||
type LocalOwnerPepperMaterialGcRecord,
|
||||
type LocalOwnerPepperMaterialGcRepository,
|
||||
type LocalOwnerPepperMaterialGcResult,
|
||||
type PrepareLocalOwnerPepperMaterialGcCommand,
|
||||
} from '@qinglong/runtime-core/local-owner-pepper-material-gc';
|
||||
import type { SecurityAuditRecord } from '@qinglong/runtime-core/security-audit';
|
||||
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
|
||||
|
||||
type QueryRow = Record<string, unknown>;
|
||||
|
||||
const GC_SELECT = `
|
||||
SELECT * FROM "QingLong3LocalOwnerPepperMaterialGc"`;
|
||||
|
||||
function text(row: QueryRow, name: string): string {
|
||||
const value = row[name];
|
||||
if (typeof value !== 'string') {
|
||||
throw new LocalOwnerPepperMaterialGcRepositoryUnavailableError();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function optionalText(row: QueryRow, name: string): string | undefined {
|
||||
const value = row[name];
|
||||
if (value === null || value === undefined) return undefined;
|
||||
return text(row, name);
|
||||
}
|
||||
|
||||
function integer(row: QueryRow, name: string): number {
|
||||
const value = row[name];
|
||||
if (typeof value !== 'number' || !Number.isSafeInteger(value)) {
|
||||
throw new LocalOwnerPepperMaterialGcRepositoryUnavailableError();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function optionalInteger(row: QueryRow, name: string): number | undefined {
|
||||
const value = row[name];
|
||||
if (value === null || value === undefined) return undefined;
|
||||
return integer(row, name);
|
||||
}
|
||||
|
||||
function gcRecord(row: QueryRow): Readonly<LocalOwnerPepperMaterialGcRecord> {
|
||||
const state = text(row, 'state');
|
||||
const completeMutationId = optionalText(row, 'complete_mutation_id');
|
||||
const completeRequestId = optionalText(row, 'complete_request_id');
|
||||
const destructionProofDigest = optionalText(row, 'destruction_proof_digest');
|
||||
const completedAtMs = optionalInteger(row, 'completed_at_ms');
|
||||
if (
|
||||
(state !== 'prepared' && state !== 'completed') ||
|
||||
(state === 'prepared' &&
|
||||
(completeMutationId !== undefined ||
|
||||
completeRequestId !== undefined ||
|
||||
destructionProofDigest !== undefined ||
|
||||
completedAtMs !== undefined)) ||
|
||||
(state === 'completed' &&
|
||||
(completeMutationId === undefined ||
|
||||
completeRequestId === undefined ||
|
||||
destructionProofDigest === undefined ||
|
||||
completedAtMs === undefined))
|
||||
) {
|
||||
throw new LocalOwnerPepperMaterialGcRepositoryUnavailableError();
|
||||
}
|
||||
const retentionPolicy = Object.freeze({
|
||||
version: 1 as const,
|
||||
acknowledgementRetentionMs: integer(row, 'acknowledgement_retention_ms'),
|
||||
auditRetentionMs: integer(row, 'audit_retention_ms'),
|
||||
backupRetentionMs: integer(row, 'backup_retention_ms'),
|
||||
});
|
||||
if (
|
||||
integer(row, 'retention_policy_version') !== 1 ||
|
||||
localOwnerPepperMaterialGcRetentionPolicyDigest(retentionPolicy) !==
|
||||
text(row, 'retention_policy_digest')
|
||||
) {
|
||||
throw new LocalOwnerPepperMaterialGcRepositoryUnavailableError();
|
||||
}
|
||||
return Object.freeze({
|
||||
prepareMutationId: text(row, 'prepare_mutation_id'),
|
||||
prepareRequestId: text(row, 'prepare_request_id'),
|
||||
pepperKeyId: text(row, 'pepper_key_id'),
|
||||
materialDigest: text(row, 'material_digest'),
|
||||
backupMaterialDigest: text(row, 'backup_material_digest'),
|
||||
activePepperKeyId: text(row, 'active_pepper_key_id'),
|
||||
activeGeneration: integer(row, 'active_generation'),
|
||||
activeMaterialDigest: text(row, 'active_material_digest'),
|
||||
retentionPolicy,
|
||||
retentionPolicyDigest: text(row, 'retention_policy_digest'),
|
||||
referencesInspectedAtMs: integer(row, 'references_inspected_at_ms'),
|
||||
retentionEligibleAtMs: integer(row, 'retention_eligible_at_ms'),
|
||||
preparedAtMs: integer(row, 'prepared_at_ms'),
|
||||
state,
|
||||
...(completeMutationId === undefined ? {} : { completeMutationId }),
|
||||
...(completeRequestId === undefined ? {} : { completeRequestId }),
|
||||
...(destructionProofDigest === undefined ? {} : { destructionProofDigest }),
|
||||
...(completedAtMs === undefined ? {} : { completedAtMs }),
|
||||
});
|
||||
}
|
||||
|
||||
function insertAudit(
|
||||
client: DatabaseSync,
|
||||
audit: Readonly<SecurityAuditRecord>,
|
||||
): void {
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3SecurityAuditEvents" (
|
||||
event_id, request_id, operation_id, project_id, subject_type,
|
||||
subject_id, authentication_id, outcome, reasons_json,
|
||||
fence_project_version, fence_binding_version, occurred_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
function safeRetentionTimestamp(base: number, duration: number): number {
|
||||
const result = base + duration;
|
||||
if (!Number.isSafeInteger(result) || result < 0) {
|
||||
throw new LocalOwnerPepperMaterialGcRepositoryUnavailableError();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function nullableMaximum(row: QueryRow | undefined): number | undefined {
|
||||
const value = row?.latest_at_ms;
|
||||
if (value === null || value === undefined) return undefined;
|
||||
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
||||
throw new LocalOwnerPepperMaterialGcRepositoryUnavailableError();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function retentionEligibleAt(
|
||||
client: DatabaseSync,
|
||||
pepperKeyId: string,
|
||||
retiredAtMs: number,
|
||||
command: Readonly<PrepareLocalOwnerPepperMaterialGcCommand>,
|
||||
): number {
|
||||
const acknowledgement = nullableMaximum(
|
||||
client
|
||||
.prepare(
|
||||
`SELECT MAX("acknowledged_at_ms") AS "latest_at_ms"
|
||||
FROM (
|
||||
SELECT acknowledgement."acknowledged_at_ms"
|
||||
FROM "QingLong3LocalOwnerDeliveryAcknowledgements" AS acknowledgement
|
||||
JOIN "QingLong3ApiCredentialPepperBindings" AS binding
|
||||
ON binding."credential_id" = acknowledgement."credential_id"
|
||||
WHERE acknowledgement."kind" = 'credential'
|
||||
AND binding."pepper_key_id" = ?
|
||||
UNION ALL
|
||||
SELECT compacted."acknowledged_at_ms"
|
||||
FROM "QingLong3LocalOwnerDeliveryAcknowledgementGc" AS compacted
|
||||
JOIN "QingLong3LocalIdentityProvisionings" AS provisioning
|
||||
ON provisioning."mutation_id" = compacted."provisioning_mutation_id"
|
||||
JOIN "QingLong3ApiCredentialPepperBindings" AS binding
|
||||
ON binding."credential_id" = provisioning."credential_id"
|
||||
AND binding."credential_version" = provisioning."credential_version"
|
||||
WHERE compacted."acknowledgement_kind" = 'credential'
|
||||
AND binding."pepper_key_id" = ?
|
||||
UNION ALL
|
||||
SELECT recovery."acknowledged_at_ms"
|
||||
FROM "QingLong3LocalOwnerCredentialRecoveries" AS recovery
|
||||
WHERE recovery."acknowledged_at_ms" IS NOT NULL
|
||||
AND (
|
||||
EXISTS (
|
||||
SELECT 1 FROM "QingLong3ApiCredentialPepperBindings" AS previous
|
||||
WHERE previous."credential_id" = recovery."previous_credential_id"
|
||||
AND previous."credential_version" = recovery."previous_credential_version"
|
||||
AND previous."pepper_key_id" = ?
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM "QingLong3ApiCredentialPepperBindings" AS replacement
|
||||
WHERE replacement."credential_id" = recovery."replacement_credential_id"
|
||||
AND replacement."credential_version" = recovery."replacement_credential_version"
|
||||
AND replacement."pepper_key_id" = ?
|
||||
)
|
||||
)
|
||||
)`,
|
||||
)
|
||||
.get(pepperKeyId, pepperKeyId, pepperKeyId, pepperKeyId) as
|
||||
| QueryRow
|
||||
| undefined,
|
||||
);
|
||||
const securityAudit = nullableMaximum(
|
||||
client
|
||||
.prepare(
|
||||
`SELECT MAX(audit."occurred_at_ms") AS "latest_at_ms"
|
||||
FROM "QingLong3SecurityAuditEvents" AS audit
|
||||
WHERE audit."event_id" IN (
|
||||
SELECT provisioning."audit_event_id"
|
||||
FROM "QingLong3LocalIdentityProvisionings" AS provisioning
|
||||
JOIN "QingLong3ApiCredentialPepperBindings" AS binding
|
||||
ON binding."credential_id" = provisioning."credential_id"
|
||||
AND binding."credential_version" = provisioning."credential_version"
|
||||
WHERE binding."pepper_key_id" = ?
|
||||
UNION
|
||||
SELECT recovery."issue_audit_event_id"
|
||||
FROM "QingLong3LocalOwnerCredentialRecoveries" AS recovery
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM "QingLong3ApiCredentialPepperBindings" AS binding
|
||||
WHERE binding."pepper_key_id" = ?
|
||||
AND (
|
||||
(binding."credential_id" = recovery."previous_credential_id"
|
||||
AND binding."credential_version" = recovery."previous_credential_version")
|
||||
OR (binding."credential_id" = recovery."replacement_credential_id"
|
||||
AND binding."credential_version" = recovery."replacement_credential_version")
|
||||
)
|
||||
)
|
||||
UNION
|
||||
SELECT recovery."complete_audit_event_id"
|
||||
FROM "QingLong3LocalOwnerCredentialRecoveries" AS recovery
|
||||
WHERE recovery."complete_audit_event_id" IS NOT NULL
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM "QingLong3ApiCredentialPepperBindings" AS binding
|
||||
WHERE binding."pepper_key_id" = ?
|
||||
AND (
|
||||
(binding."credential_id" = recovery."previous_credential_id"
|
||||
AND binding."credential_version" = recovery."previous_credential_version")
|
||||
OR (binding."credential_id" = recovery."replacement_credential_id"
|
||||
AND binding."credential_version" = recovery."replacement_credential_version")
|
||||
)
|
||||
)
|
||||
)`,
|
||||
)
|
||||
.get(pepperKeyId, pepperKeyId, pepperKeyId) as QueryRow | undefined,
|
||||
);
|
||||
return Math.max(
|
||||
safeRetentionTimestamp(
|
||||
retiredAtMs,
|
||||
command.retentionPolicy.backupRetentionMs,
|
||||
),
|
||||
...(acknowledgement === undefined
|
||||
? []
|
||||
: [
|
||||
safeRetentionTimestamp(
|
||||
acknowledgement,
|
||||
command.retentionPolicy.acknowledgementRetentionMs,
|
||||
),
|
||||
]),
|
||||
...(securityAudit === undefined
|
||||
? []
|
||||
: [
|
||||
safeRetentionTimestamp(
|
||||
securityAudit,
|
||||
command.retentionPolicy.auditRetentionMs,
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
function hasRuntimeReferences(
|
||||
client: DatabaseSync,
|
||||
pepperKeyId: string,
|
||||
inspectedAtMs: number,
|
||||
): boolean {
|
||||
const current = client
|
||||
.prepare(
|
||||
`SELECT 1
|
||||
FROM "QingLong3ApiCredentialPepperBindings" AS binding
|
||||
JOIN "QingLong3ApiCredentials" AS credential
|
||||
ON credential."credential_id" = binding."credential_id"
|
||||
AND credential."version" = binding."credential_version"
|
||||
WHERE binding."pepper_key_id" = ?
|
||||
AND credential."state" = 'active'
|
||||
AND credential."expires_at_ms" > ?
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM "QingLong3ApiCredentials" AS later
|
||||
WHERE later."credential_id" = credential."credential_id"
|
||||
AND later."version" > credential."version"
|
||||
)
|
||||
LIMIT 1`,
|
||||
)
|
||||
.get(pepperKeyId, inspectedAtMs);
|
||||
if (current) return true;
|
||||
return !!client
|
||||
.prepare(
|
||||
`SELECT 1
|
||||
FROM "QingLong3LocalOwnerCredentialRecoveries" AS recovery
|
||||
WHERE recovery."state" <> 'completed'
|
||||
AND (
|
||||
EXISTS (
|
||||
SELECT 1 FROM "QingLong3ApiCredentialPepperBindings" AS previous
|
||||
WHERE previous."credential_id" = recovery."previous_credential_id"
|
||||
AND previous."credential_version" = recovery."previous_credential_version"
|
||||
AND previous."pepper_key_id" = ?
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM "QingLong3ApiCredentialPepperBindings" AS replacement
|
||||
WHERE replacement."credential_id" = recovery."replacement_credential_id"
|
||||
AND replacement."credential_version" = recovery."replacement_credential_version"
|
||||
AND replacement."pepper_key_id" = ?
|
||||
)
|
||||
)
|
||||
LIMIT 1`,
|
||||
)
|
||||
.get(pepperKeyId, pepperKeyId);
|
||||
}
|
||||
|
||||
function samePrepare(
|
||||
record: Readonly<LocalOwnerPepperMaterialGcRecord>,
|
||||
command: Readonly<PrepareLocalOwnerPepperMaterialGcCommand>,
|
||||
): boolean {
|
||||
return (
|
||||
record.prepareMutationId === command.mutationId &&
|
||||
record.prepareRequestId === command.requestId &&
|
||||
record.pepperKeyId === command.pepperKeyId &&
|
||||
record.materialDigest === command.expectedMaterialDigest &&
|
||||
record.backupMaterialDigest === command.expectedBackupMaterialDigest &&
|
||||
record.activePepperKeyId === command.expectedActivePepperKeyId &&
|
||||
record.activeGeneration === command.expectedActiveGeneration &&
|
||||
record.activeMaterialDigest === command.expectedActiveMaterialDigest &&
|
||||
record.retentionPolicyDigest ===
|
||||
localOwnerPepperMaterialGcRetentionPolicyDigest(
|
||||
command.retentionPolicy,
|
||||
) &&
|
||||
record.preparedAtMs === command.preparedAtMs
|
||||
);
|
||||
}
|
||||
|
||||
function sameComplete(
|
||||
record: Readonly<LocalOwnerPepperMaterialGcRecord>,
|
||||
command: Readonly<CompleteLocalOwnerPepperMaterialGcCommand>,
|
||||
): boolean {
|
||||
return (
|
||||
record.completeMutationId === command.mutationId &&
|
||||
record.completeRequestId === command.requestId &&
|
||||
record.destructionProofDigest === command.destructionProofDigest &&
|
||||
record.completedAtMs === command.completedAtMs
|
||||
);
|
||||
}
|
||||
|
||||
function isDomainError(error: unknown): boolean {
|
||||
return (
|
||||
error instanceof LocalOwnerPepperMaterialGcMutationConflictError ||
|
||||
error instanceof LocalOwnerPepperMaterialGcInProgressError ||
|
||||
error instanceof LocalOwnerPepperMaterialGcReferenceConflictError ||
|
||||
error instanceof LocalOwnerPepperMaterialGcRetentionPendingError ||
|
||||
error instanceof LocalOwnerPepperMaterialGcRepositoryUnavailableError
|
||||
);
|
||||
}
|
||||
|
||||
export class LocalSqliteOwnerPepperMaterialGcRepository
|
||||
implements LocalOwnerPepperMaterialGcRepository
|
||||
{
|
||||
private readonly authority: LocalSqliteOperationAuthority;
|
||||
|
||||
constructor(authority: LocalSqliteOperationAuthority | DatabaseSync) {
|
||||
this.authority =
|
||||
authority instanceof LocalSqliteOperationAuthority
|
||||
? authority
|
||||
: new LocalSqliteOperationAuthority(authority);
|
||||
}
|
||||
|
||||
resolve(
|
||||
prepareMutationId: string,
|
||||
): Promise<Readonly<LocalOwnerPepperMaterialGcRecord> | null> {
|
||||
if (typeof prepareMutationId !== 'string') {
|
||||
throw new LocalOwnerPepperMaterialGcRepositoryUnavailableError();
|
||||
}
|
||||
return this.authority.enqueue(
|
||||
async () => {
|
||||
try {
|
||||
const row = this.authority.client
|
||||
.prepare(`${GC_SELECT} WHERE "prepare_mutation_id" = ?`)
|
||||
.get(prepareMutationId) as QueryRow | undefined;
|
||||
return row ? gcRecord(row) : null;
|
||||
} catch (error) {
|
||||
if (isDomainError(error)) throw error;
|
||||
throw new LocalOwnerPepperMaterialGcRepositoryUnavailableError();
|
||||
}
|
||||
},
|
||||
() => new LocalOwnerPepperMaterialGcRepositoryUnavailableError(),
|
||||
);
|
||||
}
|
||||
|
||||
prepare(
|
||||
candidate: PrepareLocalOwnerPepperMaterialGcCommand,
|
||||
): Promise<LocalOwnerPepperMaterialGcResult> {
|
||||
const command =
|
||||
normalizePrepareLocalOwnerPepperMaterialGcCommand(candidate);
|
||||
return this.authority.enqueue(
|
||||
async () => {
|
||||
const client = this.authority.client;
|
||||
try {
|
||||
client.exec('BEGIN IMMEDIATE');
|
||||
const replay = client
|
||||
.prepare(`${GC_SELECT} WHERE "prepare_mutation_id" = ?`)
|
||||
.get(command.mutationId) as QueryRow | undefined;
|
||||
if (replay) {
|
||||
const record = gcRecord(replay);
|
||||
if (!samePrepare(record, command)) {
|
||||
throw new LocalOwnerPepperMaterialGcMutationConflictError();
|
||||
}
|
||||
client.exec('COMMIT');
|
||||
return Object.freeze({ status: 'existing' as const, record });
|
||||
}
|
||||
if (
|
||||
client
|
||||
.prepare(`${GC_SELECT} WHERE "state" = 'prepared' LIMIT 1`)
|
||||
.get()
|
||||
) {
|
||||
throw new LocalOwnerPepperMaterialGcInProgressError();
|
||||
}
|
||||
const key = client
|
||||
.prepare(
|
||||
`SELECT "material_digest", "backup_digest", "retired_at_ms"
|
||||
FROM "QingLong3LocalOwnerPepperKeys"
|
||||
WHERE "pepper_key_id" = ? AND "state" = 'retired'`,
|
||||
)
|
||||
.get(command.pepperKeyId) as QueryRow | undefined;
|
||||
if (
|
||||
!key ||
|
||||
text(key, 'material_digest') !== command.expectedMaterialDigest ||
|
||||
text(key, 'backup_digest') !== command.expectedBackupMaterialDigest
|
||||
) {
|
||||
throw new LocalOwnerPepperMaterialGcMutationConflictError();
|
||||
}
|
||||
const active = client
|
||||
.prepare(
|
||||
`SELECT activation."generation", key."pepper_key_id",
|
||||
key."material_digest"
|
||||
FROM "QingLong3LocalOwnerPepperActivations" AS activation
|
||||
JOIN "QingLong3LocalOwnerPepperKeys" AS key
|
||||
ON key."pepper_key_id" = activation."active_pepper_key_id"
|
||||
WHERE key."state" = 'active'
|
||||
ORDER BY activation."generation" DESC
|
||||
LIMIT 1`,
|
||||
)
|
||||
.get() as QueryRow | undefined;
|
||||
if (
|
||||
!active ||
|
||||
integer(active, 'generation') !==
|
||||
command.expectedActiveGeneration ||
|
||||
text(active, 'pepper_key_id') !==
|
||||
command.expectedActivePepperKeyId ||
|
||||
text(active, 'material_digest') !==
|
||||
command.expectedActiveMaterialDigest
|
||||
) {
|
||||
throw new LocalOwnerPepperMaterialGcMutationConflictError();
|
||||
}
|
||||
if (
|
||||
hasRuntimeReferences(
|
||||
client,
|
||||
command.pepperKeyId,
|
||||
command.preparedAtMs,
|
||||
)
|
||||
) {
|
||||
throw new LocalOwnerPepperMaterialGcReferenceConflictError();
|
||||
}
|
||||
const eligibleAtMs = retentionEligibleAt(
|
||||
client,
|
||||
command.pepperKeyId,
|
||||
integer(key, 'retired_at_ms'),
|
||||
command,
|
||||
);
|
||||
if (command.preparedAtMs < eligibleAtMs) {
|
||||
throw new LocalOwnerPepperMaterialGcRetentionPendingError(
|
||||
eligibleAtMs,
|
||||
);
|
||||
}
|
||||
const policyDigest = localOwnerPepperMaterialGcRetentionPolicyDigest(
|
||||
command.retentionPolicy,
|
||||
);
|
||||
insertAudit(client, command.audit);
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3LocalOwnerPepperMaterialGc" (
|
||||
prepare_mutation_id, prepare_request_id, pepper_key_id,
|
||||
material_digest, backup_material_digest,
|
||||
active_pepper_key_id, active_generation,
|
||||
active_material_digest, retention_policy_version,
|
||||
acknowledgement_retention_ms, audit_retention_ms,
|
||||
backup_retention_ms, retention_policy_digest,
|
||||
references_inspected_at_ms, retention_eligible_at_ms,
|
||||
prepared_at_ms, prepare_audit_event_id, state
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, 'prepared')`,
|
||||
)
|
||||
.run(
|
||||
command.mutationId,
|
||||
command.requestId,
|
||||
command.pepperKeyId,
|
||||
command.expectedMaterialDigest,
|
||||
command.expectedBackupMaterialDigest,
|
||||
command.expectedActivePepperKeyId,
|
||||
command.expectedActiveGeneration,
|
||||
command.expectedActiveMaterialDigest,
|
||||
command.retentionPolicy.acknowledgementRetentionMs,
|
||||
command.retentionPolicy.auditRetentionMs,
|
||||
command.retentionPolicy.backupRetentionMs,
|
||||
policyDigest,
|
||||
command.preparedAtMs,
|
||||
eligibleAtMs,
|
||||
command.preparedAtMs,
|
||||
command.audit.eventId,
|
||||
);
|
||||
const inserted = client
|
||||
.prepare(`${GC_SELECT} WHERE "prepare_mutation_id" = ?`)
|
||||
.get(command.mutationId) as QueryRow | undefined;
|
||||
if (!inserted) {
|
||||
throw new LocalOwnerPepperMaterialGcRepositoryUnavailableError();
|
||||
}
|
||||
const record = gcRecord(inserted);
|
||||
client.exec('COMMIT');
|
||||
return Object.freeze({ status: 'inserted' as const, record });
|
||||
} catch (error) {
|
||||
if (client.isTransaction) client.exec('ROLLBACK');
|
||||
if (isDomainError(error)) throw error;
|
||||
throw new LocalOwnerPepperMaterialGcRepositoryUnavailableError();
|
||||
}
|
||||
},
|
||||
() => new LocalOwnerPepperMaterialGcRepositoryUnavailableError(),
|
||||
);
|
||||
}
|
||||
|
||||
complete(
|
||||
candidate: CompleteLocalOwnerPepperMaterialGcCommand,
|
||||
): Promise<LocalOwnerPepperMaterialGcResult> {
|
||||
const command =
|
||||
normalizeCompleteLocalOwnerPepperMaterialGcCommand(candidate);
|
||||
return this.authority.enqueue(
|
||||
async () => {
|
||||
const client = this.authority.client;
|
||||
try {
|
||||
client.exec('BEGIN IMMEDIATE');
|
||||
const row = client
|
||||
.prepare(`${GC_SELECT} WHERE "prepare_mutation_id" = ?`)
|
||||
.get(command.prepareMutationId) as QueryRow | undefined;
|
||||
if (!row) {
|
||||
throw new LocalOwnerPepperMaterialGcMutationConflictError();
|
||||
}
|
||||
const existing = gcRecord(row);
|
||||
if (existing.state === 'completed') {
|
||||
if (!sameComplete(existing, command)) {
|
||||
throw new LocalOwnerPepperMaterialGcMutationConflictError();
|
||||
}
|
||||
client.exec('COMMIT');
|
||||
return Object.freeze({
|
||||
status: 'existing' as const,
|
||||
record: existing,
|
||||
});
|
||||
}
|
||||
if (command.completedAtMs < existing.preparedAtMs) {
|
||||
throw new LocalOwnerPepperMaterialGcMutationConflictError();
|
||||
}
|
||||
insertAudit(client, command.audit);
|
||||
const result = client
|
||||
.prepare(
|
||||
`UPDATE "QingLong3LocalOwnerPepperMaterialGc"
|
||||
SET "state" = 'completed',
|
||||
"complete_mutation_id" = ?,
|
||||
"complete_request_id" = ?,
|
||||
"destruction_proof_digest" = ?,
|
||||
"completed_at_ms" = ?,
|
||||
"complete_audit_event_id" = ?
|
||||
WHERE "prepare_mutation_id" = ? AND "state" = 'prepared'`,
|
||||
)
|
||||
.run(
|
||||
command.mutationId,
|
||||
command.requestId,
|
||||
command.destructionProofDigest,
|
||||
command.completedAtMs,
|
||||
command.audit.eventId,
|
||||
command.prepareMutationId,
|
||||
);
|
||||
if (result.changes !== 1) {
|
||||
throw new LocalOwnerPepperMaterialGcMutationConflictError();
|
||||
}
|
||||
const completed = client
|
||||
.prepare(`${GC_SELECT} WHERE "prepare_mutation_id" = ?`)
|
||||
.get(command.prepareMutationId) as QueryRow | undefined;
|
||||
if (!completed) {
|
||||
throw new LocalOwnerPepperMaterialGcRepositoryUnavailableError();
|
||||
}
|
||||
const record = gcRecord(completed);
|
||||
client.exec('COMMIT');
|
||||
return Object.freeze({ status: 'inserted' as const, record });
|
||||
} catch (error) {
|
||||
if (client.isTransaction) client.exec('ROLLBACK');
|
||||
if (isDomainError(error)) throw error;
|
||||
throw new LocalOwnerPepperMaterialGcRepositoryUnavailableError();
|
||||
}
|
||||
},
|
||||
() => new LocalOwnerPepperMaterialGcRepositoryUnavailableError(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,566 @@
|
||||
import {
|
||||
LocalOwnerPepperCatalogFullError,
|
||||
LocalOwnerPepperGenerationConflictError,
|
||||
LocalOwnerPepperKeyNotActivatableError,
|
||||
LocalOwnerPepperMutationConflictError,
|
||||
LocalOwnerPepperRepositoryUnavailableError,
|
||||
MAX_LOCAL_OWNER_PEPPER_KEYS,
|
||||
normalizeActivateLocalOwnerPepperKeyCommand,
|
||||
normalizeRegisterLocalOwnerPepperKeyCommand,
|
||||
type ActivateLocalOwnerPepperKeyCommand,
|
||||
type ActivateLocalOwnerPepperKeyResult,
|
||||
type LocalOwnerPepperActivationRecord,
|
||||
type LocalOwnerPepperKeyRecord,
|
||||
type LocalOwnerPepperKeyState,
|
||||
type LocalOwnerPepperReferenceRepository,
|
||||
type LocalOwnerPepperReferenceSummary,
|
||||
type RegisterLocalOwnerPepperKeyCommand,
|
||||
type RegisterLocalOwnerPepperKeyResult,
|
||||
} from '@qinglong/runtime-core/local-owner-pepper';
|
||||
import { assertApiCredentialPepperKeyId } from '@qinglong/runtime-core/api-credential';
|
||||
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
|
||||
|
||||
interface KeyRow {
|
||||
pepper_key_id: unknown;
|
||||
material_digest: unknown;
|
||||
backup_digest: unknown;
|
||||
state: unknown;
|
||||
version: unknown;
|
||||
register_mutation_id: unknown;
|
||||
activate_mutation_id: unknown;
|
||||
retire_mutation_id: unknown;
|
||||
registered_at_ms: unknown;
|
||||
activated_at_ms: unknown;
|
||||
retired_at_ms: unknown;
|
||||
}
|
||||
|
||||
interface ActivationRow {
|
||||
generation: unknown;
|
||||
mutation_id: unknown;
|
||||
expected_generation: unknown;
|
||||
previous_pepper_key_id: unknown;
|
||||
active_pepper_key_id: unknown;
|
||||
material_digest: unknown;
|
||||
backup_digest: unknown;
|
||||
activated_at_ms: unknown;
|
||||
}
|
||||
|
||||
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
|
||||
const KEY_STATES = new Set<LocalOwnerPepperKeyState>([
|
||||
'recovery_required',
|
||||
'staged',
|
||||
'active',
|
||||
'retired',
|
||||
]);
|
||||
|
||||
function requiredString(value: unknown): string {
|
||||
if (typeof value !== 'string') {
|
||||
throw new LocalOwnerPepperRepositoryUnavailableError();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function optionalString(value: unknown): string | undefined {
|
||||
if (value === null) return undefined;
|
||||
return requiredString(value);
|
||||
}
|
||||
|
||||
function integer(value: unknown, minimum: number): number {
|
||||
if (
|
||||
typeof value !== 'number' ||
|
||||
!Number.isSafeInteger(value) ||
|
||||
value < minimum ||
|
||||
value > 2_147_483_647
|
||||
) {
|
||||
throw new LocalOwnerPepperRepositoryUnavailableError();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function optionalTimestamp(value: unknown): number | undefined {
|
||||
if (value === null) return undefined;
|
||||
return timestamp(value);
|
||||
}
|
||||
|
||||
function timestamp(value: unknown): number {
|
||||
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
||||
throw new LocalOwnerPepperRepositoryUnavailableError();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function digest(value: unknown): string {
|
||||
const result = requiredString(value);
|
||||
if (!DIGEST_PATTERN.test(result)) {
|
||||
throw new LocalOwnerPepperRepositoryUnavailableError();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function keyRecord(row: KeyRow): Readonly<LocalOwnerPepperKeyRecord> {
|
||||
const pepperKeyId = requiredString(row.pepper_key_id);
|
||||
const state = requiredString(row.state) as LocalOwnerPepperKeyState;
|
||||
try {
|
||||
assertApiCredentialPepperKeyId(pepperKeyId);
|
||||
} catch {
|
||||
throw new LocalOwnerPepperRepositoryUnavailableError();
|
||||
}
|
||||
if (!KEY_STATES.has(state)) {
|
||||
throw new LocalOwnerPepperRepositoryUnavailableError();
|
||||
}
|
||||
const materialDigest = optionalString(row.material_digest);
|
||||
const backupDigest = optionalString(row.backup_digest);
|
||||
if (
|
||||
(materialDigest !== undefined && !DIGEST_PATTERN.test(materialDigest)) ||
|
||||
(backupDigest !== undefined && !DIGEST_PATTERN.test(backupDigest)) ||
|
||||
(materialDigest === undefined) !== (backupDigest === undefined)
|
||||
) {
|
||||
throw new LocalOwnerPepperRepositoryUnavailableError();
|
||||
}
|
||||
return Object.freeze({
|
||||
pepperKeyId,
|
||||
...(materialDigest === undefined ? {} : { materialDigest }),
|
||||
...(backupDigest === undefined ? {} : { backupDigest }),
|
||||
state,
|
||||
version: integer(row.version, 1),
|
||||
...(optionalString(row.register_mutation_id) === undefined
|
||||
? {}
|
||||
: { registerMutationId: requiredString(row.register_mutation_id) }),
|
||||
...(optionalString(row.activate_mutation_id) === undefined
|
||||
? {}
|
||||
: { activateMutationId: requiredString(row.activate_mutation_id) }),
|
||||
...(optionalString(row.retire_mutation_id) === undefined
|
||||
? {}
|
||||
: { retireMutationId: requiredString(row.retire_mutation_id) }),
|
||||
registeredAtMs: timestamp(row.registered_at_ms),
|
||||
...(optionalTimestamp(row.activated_at_ms) === undefined
|
||||
? {}
|
||||
: { activatedAtMs: timestamp(row.activated_at_ms) }),
|
||||
...(optionalTimestamp(row.retired_at_ms) === undefined
|
||||
? {}
|
||||
: { retiredAtMs: timestamp(row.retired_at_ms) }),
|
||||
});
|
||||
}
|
||||
|
||||
function activationRecord(
|
||||
row: ActivationRow,
|
||||
): Readonly<LocalOwnerPepperActivationRecord> {
|
||||
const activePepperKeyId = requiredString(row.active_pepper_key_id);
|
||||
const previousPepperKeyId = optionalString(row.previous_pepper_key_id);
|
||||
try {
|
||||
assertApiCredentialPepperKeyId(activePepperKeyId);
|
||||
if (previousPepperKeyId !== undefined) {
|
||||
assertApiCredentialPepperKeyId(previousPepperKeyId);
|
||||
}
|
||||
} catch {
|
||||
throw new LocalOwnerPepperRepositoryUnavailableError();
|
||||
}
|
||||
return Object.freeze({
|
||||
generation: integer(row.generation, 1),
|
||||
mutationId: requiredString(row.mutation_id),
|
||||
expectedGeneration: integer(row.expected_generation, 0),
|
||||
...(previousPepperKeyId === undefined ? {} : { previousPepperKeyId }),
|
||||
activePepperKeyId,
|
||||
materialDigest: digest(row.material_digest),
|
||||
backupDigest: digest(row.backup_digest),
|
||||
activatedAtMs: timestamp(row.activated_at_ms),
|
||||
});
|
||||
}
|
||||
|
||||
function sameRegistration(
|
||||
key: LocalOwnerPepperKeyRecord,
|
||||
command: RegisterLocalOwnerPepperKeyCommand,
|
||||
): boolean {
|
||||
return (
|
||||
key.pepperKeyId === command.pepperKeyId &&
|
||||
key.materialDigest === command.materialDigest &&
|
||||
key.backupDigest === command.backupDigest &&
|
||||
key.registerMutationId === command.mutationId &&
|
||||
key.registeredAtMs === command.registeredAtMs
|
||||
);
|
||||
}
|
||||
|
||||
function sameActivation(
|
||||
activation: LocalOwnerPepperActivationRecord,
|
||||
command: ActivateLocalOwnerPepperKeyCommand,
|
||||
): boolean {
|
||||
return (
|
||||
activation.mutationId === command.mutationId &&
|
||||
activation.activePepperKeyId === command.pepperKeyId &&
|
||||
activation.expectedGeneration === command.expectedGeneration &&
|
||||
activation.previousPepperKeyId === command.expectedActivePepperKeyId &&
|
||||
activation.activatedAtMs === command.activatedAtMs
|
||||
);
|
||||
}
|
||||
|
||||
const KEY_SELECT = `
|
||||
SELECT pepper_key_id, material_digest, backup_digest, state, version,
|
||||
register_mutation_id, activate_mutation_id, retire_mutation_id,
|
||||
registered_at_ms, activated_at_ms, retired_at_ms
|
||||
FROM "QingLong3LocalOwnerPepperKeys"`;
|
||||
|
||||
const ACTIVATION_SELECT = `
|
||||
SELECT generation, mutation_id, expected_generation,
|
||||
previous_pepper_key_id, active_pepper_key_id,
|
||||
material_digest, backup_digest, activated_at_ms
|
||||
FROM "QingLong3LocalOwnerPepperActivations"`;
|
||||
|
||||
export class LocalSqliteOwnerPepperRepository
|
||||
implements LocalOwnerPepperReferenceRepository
|
||||
{
|
||||
private readonly authority: LocalSqliteOperationAuthority;
|
||||
|
||||
constructor(authority: LocalSqliteOperationAuthority) {
|
||||
this.authority = authority;
|
||||
}
|
||||
|
||||
resolveKey(
|
||||
pepperKeyId: string,
|
||||
): Promise<Readonly<LocalOwnerPepperKeyRecord> | null> {
|
||||
try {
|
||||
assertApiCredentialPepperKeyId(pepperKeyId);
|
||||
} catch {
|
||||
throw new LocalOwnerPepperRepositoryUnavailableError();
|
||||
}
|
||||
return this.authority.enqueue(
|
||||
async () => {
|
||||
try {
|
||||
const row = this.authority.client
|
||||
.prepare(`${KEY_SELECT} WHERE pepper_key_id = ?`)
|
||||
.get(pepperKeyId) as KeyRow | undefined;
|
||||
return row ? keyRecord(row) : null;
|
||||
} catch (error) {
|
||||
if (error instanceof LocalOwnerPepperRepositoryUnavailableError) {
|
||||
throw error;
|
||||
}
|
||||
throw new LocalOwnerPepperRepositoryUnavailableError();
|
||||
}
|
||||
},
|
||||
() => new LocalOwnerPepperRepositoryUnavailableError(),
|
||||
);
|
||||
}
|
||||
|
||||
resolveActive(): Promise<Readonly<LocalOwnerPepperActivationRecord> | null> {
|
||||
return this.authority.enqueue(
|
||||
async () => {
|
||||
try {
|
||||
const row = this.authority.client
|
||||
.prepare(`${ACTIVATION_SELECT} ORDER BY generation DESC LIMIT 1`)
|
||||
.get() as ActivationRow | undefined;
|
||||
return row ? activationRecord(row) : null;
|
||||
} catch (error) {
|
||||
if (error instanceof LocalOwnerPepperRepositoryUnavailableError) {
|
||||
throw error;
|
||||
}
|
||||
throw new LocalOwnerPepperRepositoryUnavailableError();
|
||||
}
|
||||
},
|
||||
() => new LocalOwnerPepperRepositoryUnavailableError(),
|
||||
);
|
||||
}
|
||||
|
||||
inspectReferences(
|
||||
pepperKeyId: string,
|
||||
inspectedAtMs: number,
|
||||
): Promise<Readonly<LocalOwnerPepperReferenceSummary>> {
|
||||
try {
|
||||
assertApiCredentialPepperKeyId(pepperKeyId);
|
||||
} catch {
|
||||
throw new LocalOwnerPepperRepositoryUnavailableError();
|
||||
}
|
||||
if (!Number.isSafeInteger(inspectedAtMs) || inspectedAtMs < 0) {
|
||||
throw new LocalOwnerPepperRepositoryUnavailableError();
|
||||
}
|
||||
return this.authority.enqueue(
|
||||
async () => {
|
||||
try {
|
||||
const keyRow = this.authority.client
|
||||
.prepare(`${KEY_SELECT} WHERE pepper_key_id = ?`)
|
||||
.get(pepperKeyId) as KeyRow | undefined;
|
||||
if (!keyRow) throw new LocalOwnerPepperRepositoryUnavailableError();
|
||||
const key = keyRecord(keyRow);
|
||||
const historical = this.authority.client
|
||||
.prepare(
|
||||
`SELECT COUNT(*) AS count
|
||||
FROM "QingLong3ApiCredentialPepperBindings"
|
||||
WHERE "pepper_key_id" = ?`,
|
||||
)
|
||||
.get(pepperKeyId) as { count?: unknown } | undefined;
|
||||
const current = this.authority.client
|
||||
.prepare(
|
||||
`SELECT COUNT(*) AS count
|
||||
FROM "QingLong3ApiCredentialPepperBindings" AS binding
|
||||
JOIN "QingLong3ApiCredentials" AS credential
|
||||
ON credential."credential_id" = binding."credential_id"
|
||||
AND credential."version" = binding."credential_version"
|
||||
WHERE binding."pepper_key_id" = ?
|
||||
AND credential."state" = 'active'
|
||||
AND credential."expires_at_ms" > ?
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM "QingLong3ApiCredentials" AS later
|
||||
WHERE later."credential_id" = credential."credential_id"
|
||||
AND later."version" > credential."version"
|
||||
)`,
|
||||
)
|
||||
.get(pepperKeyId, inspectedAtMs) as { count?: unknown } | undefined;
|
||||
const inFlight = this.authority.client
|
||||
.prepare(
|
||||
`SELECT COUNT(*) AS count
|
||||
FROM "QingLong3LocalOwnerCredentialRecoveries" AS recovery
|
||||
WHERE recovery."state" <> 'completed'
|
||||
AND (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM "QingLong3ApiCredentialPepperBindings" AS previous
|
||||
WHERE previous."credential_id" = recovery."previous_credential_id"
|
||||
AND previous."credential_version" = recovery."previous_credential_version"
|
||||
AND previous."pepper_key_id" = ?
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM "QingLong3ApiCredentialPepperBindings" AS replacement
|
||||
WHERE replacement."credential_id" = recovery."replacement_credential_id"
|
||||
AND replacement."credential_version" = recovery."replacement_credential_version"
|
||||
AND replacement."pepper_key_id" = ?
|
||||
)
|
||||
)`,
|
||||
)
|
||||
.get(pepperKeyId, pepperKeyId) as { count?: unknown } | undefined;
|
||||
const currentCredentialReferences = integer(current?.count, 0);
|
||||
const inFlightRecoveryReferences = integer(inFlight?.count, 0);
|
||||
return Object.freeze({
|
||||
pepperKeyId,
|
||||
inspectedAtMs,
|
||||
currentCredentialReferences,
|
||||
inFlightRecoveryReferences,
|
||||
historicalCredentialReferences: integer(historical?.count, 0),
|
||||
runtimeReferencesClear:
|
||||
key.state === 'retired' &&
|
||||
currentCredentialReferences === 0 &&
|
||||
inFlightRecoveryReferences === 0,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof LocalOwnerPepperRepositoryUnavailableError) {
|
||||
throw error;
|
||||
}
|
||||
throw new LocalOwnerPepperRepositoryUnavailableError();
|
||||
}
|
||||
},
|
||||
() => new LocalOwnerPepperRepositoryUnavailableError(),
|
||||
);
|
||||
}
|
||||
|
||||
register(
|
||||
input: RegisterLocalOwnerPepperKeyCommand,
|
||||
): Promise<RegisterLocalOwnerPepperKeyResult> {
|
||||
const command = normalizeRegisterLocalOwnerPepperKeyCommand(input);
|
||||
return this.authority.enqueue(
|
||||
async () => {
|
||||
const client = this.authority.client;
|
||||
client.exec('BEGIN IMMEDIATE');
|
||||
try {
|
||||
const replay = client
|
||||
.prepare(`${KEY_SELECT} WHERE register_mutation_id = ?`)
|
||||
.get(command.mutationId) as KeyRow | undefined;
|
||||
if (replay) {
|
||||
const key = keyRecord(replay);
|
||||
if (!sameRegistration(key, command)) {
|
||||
throw new LocalOwnerPepperMutationConflictError();
|
||||
}
|
||||
client.exec('COMMIT');
|
||||
return Object.freeze({ status: 'existing' as const, key });
|
||||
}
|
||||
const existingRow = client
|
||||
.prepare(`${KEY_SELECT} WHERE pepper_key_id = ?`)
|
||||
.get(command.pepperKeyId) as KeyRow | undefined;
|
||||
if (existingRow) {
|
||||
const existing = keyRecord(existingRow);
|
||||
if (existing.state !== 'recovery_required') {
|
||||
throw new LocalOwnerPepperMutationConflictError();
|
||||
}
|
||||
const changed = client
|
||||
.prepare(
|
||||
`UPDATE "QingLong3LocalOwnerPepperKeys"
|
||||
SET material_digest = ?, backup_digest = ?, state = 'staged',
|
||||
version = version + 1, register_mutation_id = ?,
|
||||
registered_at_ms = ?
|
||||
WHERE pepper_key_id = ? AND version = ?
|
||||
AND state = 'recovery_required'`,
|
||||
)
|
||||
.run(
|
||||
command.materialDigest,
|
||||
command.backupDigest,
|
||||
command.mutationId,
|
||||
command.registeredAtMs,
|
||||
command.pepperKeyId,
|
||||
existing.version,
|
||||
);
|
||||
if (changed.changes !== 1) {
|
||||
throw new LocalOwnerPepperMutationConflictError();
|
||||
}
|
||||
} else {
|
||||
const count = client
|
||||
.prepare(
|
||||
`SELECT COUNT(*) AS count FROM "QingLong3LocalOwnerPepperKeys"`,
|
||||
)
|
||||
.get() as { count?: unknown } | undefined;
|
||||
if (integer(count?.count, 0) >= MAX_LOCAL_OWNER_PEPPER_KEYS) {
|
||||
throw new LocalOwnerPepperCatalogFullError();
|
||||
}
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3LocalOwnerPepperKeys" (
|
||||
pepper_key_id, material_digest, backup_digest, state,
|
||||
version, register_mutation_id, registered_at_ms
|
||||
) VALUES (?, ?, ?, 'staged', 1, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
command.pepperKeyId,
|
||||
command.materialDigest,
|
||||
command.backupDigest,
|
||||
command.mutationId,
|
||||
command.registeredAtMs,
|
||||
);
|
||||
}
|
||||
const key = keyRecord(
|
||||
client
|
||||
.prepare(`${KEY_SELECT} WHERE pepper_key_id = ?`)
|
||||
.get(command.pepperKeyId) as unknown as KeyRow,
|
||||
);
|
||||
client.exec('COMMIT');
|
||||
return Object.freeze({ status: 'inserted' as const, key });
|
||||
} catch (error) {
|
||||
if (client.isTransaction) client.exec('ROLLBACK');
|
||||
if (
|
||||
error instanceof LocalOwnerPepperMutationConflictError ||
|
||||
error instanceof LocalOwnerPepperCatalogFullError ||
|
||||
error instanceof LocalOwnerPepperRepositoryUnavailableError
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
throw new LocalOwnerPepperRepositoryUnavailableError();
|
||||
}
|
||||
},
|
||||
() => new LocalOwnerPepperRepositoryUnavailableError(),
|
||||
);
|
||||
}
|
||||
|
||||
activate(
|
||||
input: ActivateLocalOwnerPepperKeyCommand,
|
||||
): Promise<ActivateLocalOwnerPepperKeyResult> {
|
||||
const command = normalizeActivateLocalOwnerPepperKeyCommand(input);
|
||||
return this.authority.enqueue(
|
||||
async () => {
|
||||
const client = this.authority.client;
|
||||
client.exec('BEGIN IMMEDIATE');
|
||||
try {
|
||||
const replay = client
|
||||
.prepare(`${ACTIVATION_SELECT} WHERE mutation_id = ?`)
|
||||
.get(command.mutationId) as ActivationRow | undefined;
|
||||
if (replay) {
|
||||
const activation = activationRecord(replay);
|
||||
if (!sameActivation(activation, command)) {
|
||||
throw new LocalOwnerPepperMutationConflictError();
|
||||
}
|
||||
client.exec('COMMIT');
|
||||
return Object.freeze({ status: 'existing' as const, activation });
|
||||
}
|
||||
const currentRow = client
|
||||
.prepare(`${ACTIVATION_SELECT} ORDER BY generation DESC LIMIT 1`)
|
||||
.get() as ActivationRow | undefined;
|
||||
const current = currentRow ? activationRecord(currentRow) : null;
|
||||
const generation = current?.generation ?? 0;
|
||||
if (
|
||||
generation !== command.expectedGeneration ||
|
||||
current?.activePepperKeyId !== command.expectedActivePepperKeyId
|
||||
) {
|
||||
throw new LocalOwnerPepperGenerationConflictError();
|
||||
}
|
||||
const targetRow = client
|
||||
.prepare(`${KEY_SELECT} WHERE pepper_key_id = ?`)
|
||||
.get(command.pepperKeyId) as KeyRow | undefined;
|
||||
const target = targetRow ? keyRecord(targetRow) : null;
|
||||
if (
|
||||
!target ||
|
||||
target.state !== 'staged' ||
|
||||
!target.materialDigest ||
|
||||
!target.backupDigest ||
|
||||
command.activatedAtMs < target.registeredAtMs
|
||||
) {
|
||||
throw new LocalOwnerPepperKeyNotActivatableError();
|
||||
}
|
||||
if (current) {
|
||||
const retired = client
|
||||
.prepare(
|
||||
`UPDATE "QingLong3LocalOwnerPepperKeys"
|
||||
SET state = 'retired', version = version + 1,
|
||||
retire_mutation_id = ?, retired_at_ms = ?
|
||||
WHERE pepper_key_id = ? AND state = 'active'`,
|
||||
)
|
||||
.run(
|
||||
command.mutationId,
|
||||
command.activatedAtMs,
|
||||
current.activePepperKeyId,
|
||||
);
|
||||
if (retired.changes !== 1) {
|
||||
throw new LocalOwnerPepperGenerationConflictError();
|
||||
}
|
||||
}
|
||||
const activated = client
|
||||
.prepare(
|
||||
`UPDATE "QingLong3LocalOwnerPepperKeys"
|
||||
SET state = 'active', version = version + 1,
|
||||
activate_mutation_id = ?, activated_at_ms = ?
|
||||
WHERE pepper_key_id = ? AND version = ? AND state = 'staged'`,
|
||||
)
|
||||
.run(
|
||||
command.mutationId,
|
||||
command.activatedAtMs,
|
||||
command.pepperKeyId,
|
||||
target.version,
|
||||
);
|
||||
if (activated.changes !== 1) {
|
||||
throw new LocalOwnerPepperGenerationConflictError();
|
||||
}
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3LocalOwnerPepperActivations" (
|
||||
generation, mutation_id, expected_generation,
|
||||
previous_pepper_key_id, active_pepper_key_id,
|
||||
material_digest, backup_digest, activated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
generation + 1,
|
||||
command.mutationId,
|
||||
command.expectedGeneration,
|
||||
current?.activePepperKeyId ?? null,
|
||||
command.pepperKeyId,
|
||||
target.materialDigest,
|
||||
target.backupDigest,
|
||||
command.activatedAtMs,
|
||||
);
|
||||
const activation = activationRecord(
|
||||
client
|
||||
.prepare(`${ACTIVATION_SELECT} WHERE mutation_id = ?`)
|
||||
.get(command.mutationId) as unknown as ActivationRow,
|
||||
);
|
||||
client.exec('COMMIT');
|
||||
return Object.freeze({ status: 'inserted' as const, activation });
|
||||
} catch (error) {
|
||||
if (client.isTransaction) client.exec('ROLLBACK');
|
||||
if (
|
||||
error instanceof LocalOwnerPepperMutationConflictError ||
|
||||
error instanceof LocalOwnerPepperGenerationConflictError ||
|
||||
error instanceof LocalOwnerPepperKeyNotActivatableError ||
|
||||
error instanceof LocalOwnerPepperRepositoryUnavailableError
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
throw new LocalOwnerPepperRepositoryUnavailableError();
|
||||
}
|
||||
},
|
||||
() => new LocalOwnerPepperRepositoryUnavailableError(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { LocalOwnerBootstrapRepository } from '@qinglong/runtime-core/local-owner-bootstrap';
|
||||
import type { LocalOwnerDeliveryAcknowledgementGcRepository } from '@qinglong/runtime-core/local-owner-delivery-acknowledgement-gc';
|
||||
import {
|
||||
assertLocalSqliteOptions,
|
||||
assertLocalSqlitePathBoundary,
|
||||
LocalSqliteConfigurationError,
|
||||
openLocalSqliteClient,
|
||||
type LocalSqliteDatabaseOptions,
|
||||
type LocalSqliteProfile,
|
||||
} from '../storage/config';
|
||||
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
|
||||
import { LocalSqliteOwnerBootstrapRepository } from '../local-owner/ownerBootstrapRepository';
|
||||
import { LocalSqliteOwnerDeliveryAcknowledgementGcRepository } from '../local-owner/ownerDeliveryAcknowledgementGcRepository';
|
||||
import {
|
||||
auditLocalSqliteReadiness,
|
||||
type LocalSqliteReadinessEvidence,
|
||||
} from '../readiness/readiness';
|
||||
|
||||
/** Short-lived authority for one reviewed acknowledgement compaction. */
|
||||
export interface LocalSqliteAcknowledgementGcDatabase {
|
||||
readonly profile: LocalSqliteProfile;
|
||||
readonly readiness: LocalSqliteReadinessEvidence;
|
||||
readonly ownerBootstrap: LocalOwnerBootstrapRepository;
|
||||
readonly acknowledgementGc: LocalOwnerDeliveryAcknowledgementGcRepository;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export async function openLocalSqliteAcknowledgementGcDatabase(
|
||||
options: LocalSqliteDatabaseOptions,
|
||||
): Promise<LocalSqliteAcknowledgementGcDatabase> {
|
||||
assertLocalSqliteOptions(options);
|
||||
assertLocalSqlitePathBoundary(options.databasePath, false);
|
||||
const client = openLocalSqliteClient(options, false);
|
||||
try {
|
||||
const readiness = await auditLocalSqliteReadiness(client);
|
||||
const authority = new LocalSqliteOperationAuthority(client);
|
||||
const ownerBootstrap = new LocalSqliteOwnerBootstrapRepository(authority);
|
||||
const acknowledgementGc =
|
||||
new LocalSqliteOwnerDeliveryAcknowledgementGcRepository(authority);
|
||||
let closePromise: Promise<void> | undefined;
|
||||
return Object.freeze({
|
||||
profile: options.profile,
|
||||
readiness,
|
||||
ownerBootstrap,
|
||||
acknowledgementGc,
|
||||
close() {
|
||||
if (closePromise) return closePromise;
|
||||
closePromise = authority.close();
|
||||
return closePromise;
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (client.isOpen) client.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
LocalSqliteConfigurationError,
|
||||
type LocalSqliteDatabaseOptions,
|
||||
type LocalSqliteProfile,
|
||||
};
|
||||
export type { LocalSqliteReadinessEvidence } from '../readiness/readiness';
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { LocalOwnerPepperReferenceRepository } from '@qinglong/runtime-core/local-owner-pepper';
|
||||
import type { LocalOwnerPepperMaterialGcRepository } from '@qinglong/runtime-core/local-owner-pepper-material-gc';
|
||||
import {
|
||||
assertLocalSqliteOptions,
|
||||
assertLocalSqlitePathBoundary,
|
||||
LocalSqliteConfigurationError,
|
||||
openLocalSqliteClient,
|
||||
type LocalSqliteDatabaseOptions,
|
||||
type LocalSqliteProfile,
|
||||
} from '../storage/config';
|
||||
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
|
||||
import { LocalSqliteOwnerPepperMaterialGcRepository } from '../local-owner/ownerPepperMaterialGcRepository';
|
||||
import { LocalSqliteOwnerPepperRepository } from '../local-owner/ownerPepperRepository';
|
||||
import {
|
||||
auditLocalSqliteReadiness,
|
||||
type LocalSqliteReadinessEvidence,
|
||||
} from '../readiness/readiness';
|
||||
|
||||
/** Short-lived authority for one reviewed pepper material GC operation. */
|
||||
export interface LocalSqlitePepperGcDatabase {
|
||||
readonly profile: LocalSqliteProfile;
|
||||
readonly readiness: LocalSqliteReadinessEvidence;
|
||||
readonly ownerPepper: LocalOwnerPepperReferenceRepository;
|
||||
readonly materialGc: LocalOwnerPepperMaterialGcRepository;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export async function openLocalSqlitePepperGcDatabase(
|
||||
options: LocalSqliteDatabaseOptions,
|
||||
): Promise<LocalSqlitePepperGcDatabase> {
|
||||
assertLocalSqliteOptions(options);
|
||||
assertLocalSqlitePathBoundary(options.databasePath, false);
|
||||
const client = openLocalSqliteClient(options, false);
|
||||
try {
|
||||
const readiness = await auditLocalSqliteReadiness(client);
|
||||
const authority = new LocalSqliteOperationAuthority(client);
|
||||
const ownerPepper = new LocalSqliteOwnerPepperRepository(authority);
|
||||
const materialGc = new LocalSqliteOwnerPepperMaterialGcRepository(
|
||||
authority,
|
||||
);
|
||||
let closePromise: Promise<void> | undefined;
|
||||
return Object.freeze({
|
||||
profile: options.profile,
|
||||
readiness,
|
||||
ownerPepper,
|
||||
materialGc,
|
||||
close() {
|
||||
if (closePromise) return closePromise;
|
||||
closePromise = authority.close();
|
||||
return closePromise;
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (client.isOpen) client.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
LocalSqliteConfigurationError,
|
||||
type LocalSqliteDatabaseOptions,
|
||||
type LocalSqliteProfile,
|
||||
};
|
||||
export type { LocalSqliteReadinessEvidence } from '../readiness/readiness';
|
||||
@@ -0,0 +1,256 @@
|
||||
import {
|
||||
runMigrationStream,
|
||||
type MigrationStreamDefinition,
|
||||
} from '@qinglong/runtime-core/migration-stream';
|
||||
import type { DatabaseSync } from 'node:sqlite';
|
||||
import fs from 'node:fs';
|
||||
import {
|
||||
assertLocalSqliteOptions,
|
||||
assertLocalSqlitePathBoundary,
|
||||
openLocalSqliteClient,
|
||||
type LocalSqliteDatabaseOptions,
|
||||
} from '../storage/config';
|
||||
import { local0001RunCoreMigration } from '../migrations/0001-run-core';
|
||||
import { local0002CapabilityMigration } from '../migrations/0002-capability';
|
||||
import { local0003CompletionReceiptJournalMigration } from '../migrations/0003-completion-receipt-journal';
|
||||
import { local0004CapabilityV2Migration } from '../migrations/0004-capability-v2';
|
||||
import { local0005LocalDispatchPlanMigration } from '../migrations/0005-local-dispatch-plan';
|
||||
import { local0006CapabilityV3Migration } from '../migrations/0006-capability-v3';
|
||||
import { local0007LocalSecretEnvelopesMigration } from '../migrations/0007-local-secret-envelopes';
|
||||
import { local0008CapabilityV4Migration } from '../migrations/0008-capability-v4';
|
||||
import { local0009LocalProjectPolicyAuditMigration } from '../migrations/0009-local-project-policy-audit';
|
||||
import { local0010CapabilityV5Migration } from '../migrations/0010-capability-v5';
|
||||
import { local0011LocalIdentityCredentialMigration } from '../migrations/0011-local-identity-credential';
|
||||
import { local0012CapabilityV6Migration } from '../migrations/0012-capability-v6';
|
||||
import { local0013LocalOwnerBootstrapMigration } from '../migrations/0013-local-owner-bootstrap';
|
||||
import { local0014CapabilityV7Migration } from '../migrations/0014-capability-v7';
|
||||
import { local0015LocalOwnerDeliveryAcknowledgementsMigration } from '../migrations/0015-local-owner-delivery-acknowledgements';
|
||||
import { local0016CapabilityV8Migration } from '../migrations/0016-capability-v8';
|
||||
import { local0017ApiCredentialPepperBindingsMigration } from '../migrations/0017-api-credential-pepper-bindings';
|
||||
import { local0018CapabilityV9Migration } from '../migrations/0018-capability-v9';
|
||||
import { local0019LocalOwnerPepperCatalogMigration } from '../migrations/0019-local-owner-pepper-catalog';
|
||||
import { local0020CapabilityV10Migration } from '../migrations/0020-capability-v10';
|
||||
import { local0021LocalOwnerCredentialRecoveryMigration } from '../migrations/0021-local-owner-credential-recovery';
|
||||
import { local0022CapabilityV11Migration } from '../migrations/0022-capability-v11';
|
||||
import { local0023LocalOwnerPepperMaterialGcMigration } from '../migrations/0023-local-owner-pepper-material-gc';
|
||||
import { local0024CapabilityV12Migration } from '../migrations/0024-capability-v12';
|
||||
import { local0025LocalOwnerDeliveryAcknowledgementGcMigration } from '../migrations/0025-local-owner-delivery-acknowledgement-gc';
|
||||
import { local0026CapabilityV13Migration } from '../migrations/0026-capability-v13';
|
||||
import { local0027TaskDefinitionsMigration } from '../migrations/0027-task-definitions';
|
||||
import { local0028CapabilityV14Migration } from '../migrations/0028-capability-v14';
|
||||
import { local0029LocalExecutionRevisionDigestMigration } from '../migrations/0029-local-execution-revision-digest';
|
||||
import { local0030CapabilityV15Migration } from '../migrations/0030-capability-v15';
|
||||
import { local0031TriggerDefinitionsMigration } from '../migrations/0031-trigger-definitions';
|
||||
import { local0032CapabilityV16Migration } from '../migrations/0032-capability-v16';
|
||||
import { local0033LegacyAdoptionLedgerMigration } from '../migrations/0033-legacy-adoption-ledger';
|
||||
import { local0034CapabilityV17Migration } from '../migrations/0034-capability-v17';
|
||||
import { local0035LocalSchedulerMigration } from '../migrations/0035-local-scheduler';
|
||||
import { local0036CapabilityV18Migration } from '../migrations/0036-capability-v18';
|
||||
import { local0037PluginPackageInstallsMigration } from '../migrations/0037-plugin-package-installs';
|
||||
import { local0038CapabilityV19Migration } from '../migrations/0038-capability-v19';
|
||||
import { local0039ApprovedActionsMigration } from '../migrations/0039-approved-actions';
|
||||
import { local0040CapabilityV20Migration } from '../migrations/0040-capability-v20';
|
||||
import { local0041PluginPackageAdmissionReceiptsMigration } from '../migrations/0041-plugin-package-admission-receipts';
|
||||
import { local0042CapabilityV21Migration } from '../migrations/0042-capability-v21';
|
||||
import { local0043ApprovedActionExecutionsAndPackageProposalsMigration } from '../migrations/0043-approved-action-executions-and-package-proposals';
|
||||
import { local0044CapabilityV22Migration } from '../migrations/0044-capability-v22';
|
||||
import { local0045PluginPackageMaterializedRevisionsMigration } from '../migrations/0045-plugin-package-materialized-revisions';
|
||||
import { local0046CapabilityV23Migration } from '../migrations/0046-capability-v23';
|
||||
import { local0047PluginPackageTaskReconciliationsMigration } from '../migrations/0047-plugin-package-task-reconciliations';
|
||||
import { local0048CapabilityV24Migration } from '../migrations/0048-capability-v24';
|
||||
import { local0049ProjectToolDefinitionSnapshotsMigration } from '../migrations/0049-project-tool-definition-snapshots';
|
||||
import { local0050CapabilityV25Migration } from '../migrations/0050-capability-v25';
|
||||
import { local0051StepRunsMigration } from '../migrations/0051-step-runs';
|
||||
import { local0052CapabilityV26Migration } from '../migrations/0052-capability-v26';
|
||||
import { local0053ToolExecutionEvidenceMigration } from '../migrations/0053-tool-execution-evidence';
|
||||
import { local0054CapabilityV27Migration } from '../migrations/0054-capability-v27';
|
||||
import { local0055ToolExecutionStartBarriersMigration } from '../migrations/0055-tool-execution-start-barriers';
|
||||
import { local0056CapabilityV28Migration } from '../migrations/0056-capability-v28';
|
||||
import { local0057ToolInvocationArtifactsMigration } from '../migrations/0057-tool-invocation-artifacts';
|
||||
import { local0058CapabilityV29Migration } from '../migrations/0058-capability-v29';
|
||||
import { local0059ToolExecutionArtifactBindingsMigration } from '../migrations/0059-tool-execution-artifact-bindings';
|
||||
import { local0060CapabilityV30Migration } from '../migrations/0060-capability-v30';
|
||||
import { local0061ToolExecutionCompletionsMigration } from '../migrations/0061-tool-execution-completions';
|
||||
import { local0062CapabilityV31Migration } from '../migrations/0062-capability-v31';
|
||||
import { local0063ToolExecutionFailureCompletionsMigration } from '../migrations/0063-tool-execution-failure-completions';
|
||||
import { local0064CapabilityV32Migration } from '../migrations/0064-capability-v32';
|
||||
import { local0065ToolResultKeyCatalogMigration } from '../migrations/0065-tool-result-key-catalog';
|
||||
import { local0066CapabilityV33Migration } from '../migrations/0066-capability-v33';
|
||||
import { local0067ToolResultRekeyOverlaysMigration } from '../migrations/0067-tool-result-rekey-overlays';
|
||||
import { local0068CapabilityV34Migration } from '../migrations/0068-capability-v34';
|
||||
import { local0069PluginPackageQuarantineMigration } from '../migrations/0069-plugin-package-quarantine';
|
||||
import { local0070CapabilityV35Migration } from '../migrations/0070-capability-v35';
|
||||
import { local0071LocalIdentityCredentialAdministrationMigration } from '../migrations/0071-local-identity-credential-administration';
|
||||
import { local0072CapabilityV36Migration } from '../migrations/0072-capability-v36';
|
||||
import { local0073LocalProjectAdministrationMigration } from '../migrations/0073-local-project-administration';
|
||||
import { local0074CapabilityV37Migration } from '../migrations/0074-capability-v37';
|
||||
import { local0075SecurityAuditCompactionsMigration } from '../migrations/0075-security-audit-compactions';
|
||||
import { local0076CapabilityV38Migration } from '../migrations/0076-capability-v38';
|
||||
import { local0077PluginPackageLifecycleMigration } from '../migrations/0077-plugin-package-lifecycle';
|
||||
import { local0078CapabilityV39Migration } from '../migrations/0078-capability-v39';
|
||||
import { local0079PluginPackageAutomationPublicationsMigration } from '../migrations/0079-plugin-package-automation-publications';
|
||||
import { local0080CapabilityV40Migration } from '../migrations/0080-capability-v40';
|
||||
import { local0081PluginPackageWorkflowAdmissionsMigration } from '../migrations/0081-plugin-package-workflow-admissions';
|
||||
import { local0082CapabilityV41Migration } from '../migrations/0082-capability-v41';
|
||||
import { local0083PluginPackageWorkflowTaskAttemptAdmissionsMigration } from '../migrations/0083-plugin-package-workflow-task-attempt-admissions';
|
||||
import { local0084CapabilityV42Migration } from '../migrations/0084-capability-v42';
|
||||
import { local0085PluginPackageWorkflowRunListIndexMigration } from '../migrations/0085-plugin-package-workflow-run-list-index';
|
||||
import { local0086CapabilityV43Migration } from '../migrations/0086-capability-v43';
|
||||
import type { LocalSqliteMigrationContext } from '../migrations/sqlMigration';
|
||||
import {
|
||||
LOCAL_SQLITE_MIGRATION_STREAM_ID,
|
||||
LocalSqliteMigrationStreamStore,
|
||||
} from './migrationStreamStore';
|
||||
import { localSqliteMigrationManifest } from './migrationManifest';
|
||||
import {
|
||||
auditLocalSqliteReadiness,
|
||||
type LocalSqliteReadinessEvidence,
|
||||
} from '../readiness/readiness';
|
||||
|
||||
export interface LocalSqliteMigrationResult {
|
||||
readonly readiness: LocalSqliteReadinessEvidence;
|
||||
}
|
||||
|
||||
export const localSqliteMigrationDefinition: MigrationStreamDefinition<LocalSqliteMigrationContext> =
|
||||
Object.freeze({
|
||||
id: LOCAL_SQLITE_MIGRATION_STREAM_ID,
|
||||
dialect: 'sqlite',
|
||||
migrationIdScheme: 'sqlite-numbered',
|
||||
checksumScheme: 'sha256',
|
||||
migrations: Object.freeze([
|
||||
local0001RunCoreMigration,
|
||||
local0002CapabilityMigration,
|
||||
local0003CompletionReceiptJournalMigration,
|
||||
local0004CapabilityV2Migration,
|
||||
local0005LocalDispatchPlanMigration,
|
||||
local0006CapabilityV3Migration,
|
||||
local0007LocalSecretEnvelopesMigration,
|
||||
local0008CapabilityV4Migration,
|
||||
local0009LocalProjectPolicyAuditMigration,
|
||||
local0010CapabilityV5Migration,
|
||||
local0011LocalIdentityCredentialMigration,
|
||||
local0012CapabilityV6Migration,
|
||||
local0013LocalOwnerBootstrapMigration,
|
||||
local0014CapabilityV7Migration,
|
||||
local0015LocalOwnerDeliveryAcknowledgementsMigration,
|
||||
local0016CapabilityV8Migration,
|
||||
local0017ApiCredentialPepperBindingsMigration,
|
||||
local0018CapabilityV9Migration,
|
||||
local0019LocalOwnerPepperCatalogMigration,
|
||||
local0020CapabilityV10Migration,
|
||||
local0021LocalOwnerCredentialRecoveryMigration,
|
||||
local0022CapabilityV11Migration,
|
||||
local0023LocalOwnerPepperMaterialGcMigration,
|
||||
local0024CapabilityV12Migration,
|
||||
local0025LocalOwnerDeliveryAcknowledgementGcMigration,
|
||||
local0026CapabilityV13Migration,
|
||||
local0027TaskDefinitionsMigration,
|
||||
local0028CapabilityV14Migration,
|
||||
local0029LocalExecutionRevisionDigestMigration,
|
||||
local0030CapabilityV15Migration,
|
||||
local0031TriggerDefinitionsMigration,
|
||||
local0032CapabilityV16Migration,
|
||||
local0033LegacyAdoptionLedgerMigration,
|
||||
local0034CapabilityV17Migration,
|
||||
local0035LocalSchedulerMigration,
|
||||
local0036CapabilityV18Migration,
|
||||
local0037PluginPackageInstallsMigration,
|
||||
local0038CapabilityV19Migration,
|
||||
local0039ApprovedActionsMigration,
|
||||
local0040CapabilityV20Migration,
|
||||
local0041PluginPackageAdmissionReceiptsMigration,
|
||||
local0042CapabilityV21Migration,
|
||||
local0043ApprovedActionExecutionsAndPackageProposalsMigration,
|
||||
local0044CapabilityV22Migration,
|
||||
local0045PluginPackageMaterializedRevisionsMigration,
|
||||
local0046CapabilityV23Migration,
|
||||
local0047PluginPackageTaskReconciliationsMigration,
|
||||
local0048CapabilityV24Migration,
|
||||
local0049ProjectToolDefinitionSnapshotsMigration,
|
||||
local0050CapabilityV25Migration,
|
||||
local0051StepRunsMigration,
|
||||
local0052CapabilityV26Migration,
|
||||
local0053ToolExecutionEvidenceMigration,
|
||||
local0054CapabilityV27Migration,
|
||||
local0055ToolExecutionStartBarriersMigration,
|
||||
local0056CapabilityV28Migration,
|
||||
local0057ToolInvocationArtifactsMigration,
|
||||
local0058CapabilityV29Migration,
|
||||
local0059ToolExecutionArtifactBindingsMigration,
|
||||
local0060CapabilityV30Migration,
|
||||
local0061ToolExecutionCompletionsMigration,
|
||||
local0062CapabilityV31Migration,
|
||||
local0063ToolExecutionFailureCompletionsMigration,
|
||||
local0064CapabilityV32Migration,
|
||||
local0065ToolResultKeyCatalogMigration,
|
||||
local0066CapabilityV33Migration,
|
||||
local0067ToolResultRekeyOverlaysMigration,
|
||||
local0068CapabilityV34Migration,
|
||||
local0069PluginPackageQuarantineMigration,
|
||||
local0070CapabilityV35Migration,
|
||||
local0071LocalIdentityCredentialAdministrationMigration,
|
||||
local0072CapabilityV36Migration,
|
||||
local0073LocalProjectAdministrationMigration,
|
||||
local0074CapabilityV37Migration,
|
||||
local0075SecurityAuditCompactionsMigration,
|
||||
local0076CapabilityV38Migration,
|
||||
local0077PluginPackageLifecycleMigration,
|
||||
local0078CapabilityV39Migration,
|
||||
local0079PluginPackageAutomationPublicationsMigration,
|
||||
local0080CapabilityV40Migration,
|
||||
local0081PluginPackageWorkflowAdmissionsMigration,
|
||||
local0082CapabilityV41Migration,
|
||||
local0083PluginPackageWorkflowTaskAttemptAdmissionsMigration,
|
||||
local0084CapabilityV42Migration,
|
||||
local0085PluginPackageWorkflowRunListIndexMigration,
|
||||
local0086CapabilityV43Migration,
|
||||
]),
|
||||
});
|
||||
|
||||
function assertReviewedManifestMatchesDefinition(): void {
|
||||
const generated = localSqliteMigrationDefinition.migrations.map(
|
||||
({ id, checksum }) => ({ id, checksum }),
|
||||
);
|
||||
if (
|
||||
localSqliteMigrationManifest.id !== localSqliteMigrationDefinition.id ||
|
||||
localSqliteMigrationManifest.dialect !==
|
||||
localSqliteMigrationDefinition.dialect ||
|
||||
JSON.stringify(localSqliteMigrationManifest.migrations) !==
|
||||
JSON.stringify(generated)
|
||||
) {
|
||||
throw new Error(
|
||||
'Local SQLite executable migrations do not match the reviewed runtime manifest',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
assertReviewedManifestMatchesDefinition();
|
||||
|
||||
export async function migrateLocalSqliteDatabase(
|
||||
client: DatabaseSync,
|
||||
): Promise<void> {
|
||||
await runMigrationStream({
|
||||
stream: localSqliteMigrationDefinition,
|
||||
store: new LocalSqliteMigrationStreamStore(client),
|
||||
});
|
||||
}
|
||||
|
||||
export { localSqliteMigrationManifest };
|
||||
|
||||
/** Short-lived migration authority; long-lived Profile hosts must not import it. */
|
||||
export async function migrateLocalSqlitePath(
|
||||
options: LocalSqliteDatabaseOptions,
|
||||
): Promise<LocalSqliteMigrationResult> {
|
||||
assertLocalSqliteOptions(options);
|
||||
assertLocalSqlitePathBoundary(options.databasePath, true);
|
||||
const client = openLocalSqliteClient(options, false);
|
||||
try {
|
||||
await migrateLocalSqliteDatabase(client);
|
||||
fs.chmodSync(options.databasePath, 0o600);
|
||||
return Object.freeze({
|
||||
readiness: await auditLocalSqliteReadiness(client),
|
||||
});
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
import type { MigrationStreamManifest } from '@qinglong/runtime-core/migration-stream';
|
||||
import { LOCAL_SQLITE_MIGRATION_STREAM_ID } from './migrationStreamStore';
|
||||
|
||||
/**
|
||||
* Runtime-safe reviewed migration history. Checksums are frozen audit facts; executable
|
||||
* SQL lives only behind the /migration entrypoint.
|
||||
*/
|
||||
export const localSqliteMigrationManifest: MigrationStreamManifest =
|
||||
Object.freeze({
|
||||
id: LOCAL_SQLITE_MIGRATION_STREAM_ID,
|
||||
dialect: 'sqlite',
|
||||
migrationIdScheme: 'sqlite-numbered',
|
||||
checksumScheme: 'sha256',
|
||||
migrations: Object.freeze([
|
||||
Object.freeze({
|
||||
id: '0001-run-core',
|
||||
checksum:
|
||||
'39568123409d2c7f0bc719418640552b71e9880bd7f04edf827c6b281111c1ed',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0002-capability',
|
||||
checksum:
|
||||
'67742152a864e4e3b01dce74b547ff47f1630fe38d955b5f98a9e9c0d1a2f85f',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0003-completion-receipt-journal',
|
||||
checksum:
|
||||
'f097d92e50be11812c8d501e173319e4142e7ea6b53b91e170f0f5bd46d0b454',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0004-capability-v2',
|
||||
checksum:
|
||||
'74f26d198e73dbf1ec46ff1a0d5874c7a2ba4bd0bcfc49270b19111c0005fe10',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0005-local-dispatch-plan',
|
||||
checksum:
|
||||
'20dea02fd01f6577624078d2ceae7f93939faa60fdb1a6bc145dab65f38388e9',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0006-capability-v3',
|
||||
checksum:
|
||||
'ba2d32b53be6f9a5684178e33bdd3af6b48fb03532832eb359a273bd5be419e5',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0007-local-secret-envelopes',
|
||||
checksum:
|
||||
'bc24730051cd8306ccb7e5ecbdb7911dd2352f8bcb4cd448a0a4eb3004b68224',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0008-capability-v4',
|
||||
checksum:
|
||||
'eee83101204917ba2651dbbf962c5f8b6783223deecf298345f39b842afbc5e1',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0009-local-project-policy-audit',
|
||||
checksum:
|
||||
'2dd3d535aa2cce27d4032ac8d0a9e0afe0e0ac05e0ed8f91328765a8ad9b3175',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0010-capability-v5',
|
||||
checksum:
|
||||
'43e4d311fa5705588a5b66f3c32f942d220b7d86a23003f7b73ddad4bf6bb12d',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0011-local-identity-credential',
|
||||
checksum:
|
||||
'9ad007caaf28a2e5310a84c0d08f8314ee8db17f309cfd323b86f763b0f349c9',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0012-capability-v6',
|
||||
checksum:
|
||||
'c0a3321d546c98f651180802e94e8d819246b5e51449b749d8f0e82710464d0c',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0013-local-owner-bootstrap',
|
||||
checksum:
|
||||
'83304887fc0a265b71e47b61d74554f63bccf0e0208d85c3e79e4cfd5dcbbe48',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0014-capability-v7',
|
||||
checksum:
|
||||
'46df0f0fc48a8510adab4b34e0697eb21572267fce46c5764f0b7e35e412d50e',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0015-local-owner-delivery-acknowledgements',
|
||||
checksum:
|
||||
'e4c5f4dd7f9a5f717054fea9f3eb3b4f0aa3c01ce66559bb55b47ab9928a5dfa',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0016-capability-v8',
|
||||
checksum:
|
||||
'd5abd73e4422555e13d989e646523d766c07c4d2261026e53097d7e756d7265a',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0017-api-credential-pepper-bindings',
|
||||
checksum:
|
||||
'32144992e70ea8ea1f45a5e88bfa7b94da6f7943349c0ec974e23e7ad14488e6',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0018-capability-v9',
|
||||
checksum:
|
||||
'da15b963e45c49c5d145d7c5c9c06ab41921ff054a5573bb5ebb698da6524562',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0019-local-owner-pepper-catalog',
|
||||
checksum:
|
||||
'b2c7afe88d02eb8e3bc0cceaa590edb252adf578f9d5a1292d5969a50847a89d',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0020-capability-v10',
|
||||
checksum:
|
||||
'c5caf45785d4efe2885ffe857a59109acdc36ec1b79ca2079b0b6274f68d541b',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0021-local-owner-credential-recovery',
|
||||
checksum:
|
||||
'7d52b983194d2078e33fef181e1cba115266ac7fb253415a71e222df873af507',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0022-capability-v11',
|
||||
checksum:
|
||||
'bcb367bc095d97a47f512c002dd12e74314b36fbbbd71862d3441b810f94d006',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0023-local-owner-pepper-material-gc',
|
||||
checksum:
|
||||
'995bf7b84d62381e1620347d3306349fcaa4c71c2072f23be504eeb93ca3e7d4',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0024-capability-v12',
|
||||
checksum:
|
||||
'40f4819c3877cdc1626fc780a5ac916f6bb6f8d3d1a1d437f55dc2fd8afe9b0b',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0025-local-owner-delivery-acknowledgement-gc',
|
||||
checksum:
|
||||
'3982b92013917606c561b6fe7a4fdaae72e4da3eae91befa1a19a44382e39507',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0026-capability-v13',
|
||||
checksum:
|
||||
'2a606be91f4f216abddbfb6b6918971a7e5cf086df7c620de60fdb40d0849340',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0027-task-definitions',
|
||||
checksum:
|
||||
'3ae15a3f964dbcbeacb311862ad52dd6489715fe87cfe1a8e8ec9bec59c2a81f',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0028-capability-v14',
|
||||
checksum:
|
||||
'408fc34ad2aa5439b05ccf7438ee98bcc952cd2c72bcbf81931a0f6598e6039d',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0029-local-execution-revision-digest',
|
||||
checksum:
|
||||
'273ebd3f10ee0f17b2037b446d67943d2c5d1e819ca61651abdb52de7245ccbf',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0030-capability-v15',
|
||||
checksum:
|
||||
'c51cbab2a4f4747b14a7ddec4629356a923b840414efb52388a7ebe7e463a369',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0031-trigger-definitions',
|
||||
checksum:
|
||||
'c3d37ccf8f62ed006f8236157c1917696d7d3ea41db1db14b662b76e2f1203da',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0032-capability-v16',
|
||||
checksum:
|
||||
'28c927e5787323b4df2b39a755de8f7c629bea46f7bd1bac2502c6de93a4aba7',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0033-legacy-adoption-ledger',
|
||||
checksum:
|
||||
'73cb0155c1321f15fbef3a22649d97278245fa1d3865391b4d053104f0102f80',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0034-capability-v17',
|
||||
checksum:
|
||||
'a973fd2471d42844a5eb8f40b9a6ae4ed2aa1507ab8cae1b9de9a783d593f0f2',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0035-local-scheduler',
|
||||
checksum:
|
||||
'eb312b03ea61f4d3fa4f8751d93ede7bf6b3c2c1893b785e8d83d24eda8f8c5e',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0036-capability-v18',
|
||||
checksum:
|
||||
'0ea27d9d5be58c03a092f5717b64d6eae8a45fb732461ca684744a220ce1c1d1',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0037-plugin-package-installs',
|
||||
checksum:
|
||||
'6d40e13043b6dddd7ef16c1562b17fa4faf3fbc832e152976ac6e5334ba26175',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0038-capability-v19',
|
||||
checksum:
|
||||
'6709d442e985ffc91ea4622c3bc322c838c022745f498f5ea94b8503d286e0c4',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0039-approved-actions',
|
||||
checksum:
|
||||
'2f0a258aaf99eb83e28af76b95529dd0c76b8f6ea28baa3d3cdbfd353ad6eef9',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0040-capability-v20',
|
||||
checksum:
|
||||
'c06ce8f021a4e1874e14deb4b824a45273a1e96f5625a487f2e6d940ce364ee2',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0041-plugin-package-admission-receipts',
|
||||
checksum:
|
||||
'fac42799fad2b80c34d49f922ccd42e5b9f213435767ae7efb865370f6799432',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0042-capability-v21',
|
||||
checksum:
|
||||
'b4789c615f92299b8dac879c75d377ddddf22e547f069ee8ccc9a16647b21410',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0043-approved-action-executions-and-package-proposals',
|
||||
checksum:
|
||||
'e0d2d5718e2e58e5cc841bff67d3b533840a727ea0d860850c67c6d4e4edbba9',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0044-capability-v22',
|
||||
checksum:
|
||||
'9ae0d6d3dde8f4e09abefab67cedd4cfdb9f5ad9725f136e69e1c9480d1bb99b',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0045-plugin-package-materialized-revisions',
|
||||
checksum:
|
||||
'935b2c1806bdb2c45fafc9813089070088c8f660f2f4b479a3866d790b161f8b',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0046-capability-v23',
|
||||
checksum:
|
||||
'5d874653fe3971a52c71d3706b89f38aefc03fe00c28fb6ba23f2249413cf8c0',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0047-plugin-package-task-reconciliations',
|
||||
checksum:
|
||||
'cc4caf2409c513d982d910ff853cbebf281e7999c8e1b2e6d00d50f0e6ebc2f2',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0048-capability-v24',
|
||||
checksum:
|
||||
'd2257f4b08f01703507c49d7b2bf197930a88eb70d6e07ab13b3627657df0031',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0049-project-tool-definition-snapshots',
|
||||
checksum:
|
||||
'7b7d02243fb3ab3fce5444dd73dba96a4e16598f082a4c0e786404b860266831',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0050-capability-v25',
|
||||
checksum:
|
||||
'9d128ecd3bac1a45e7bea1cde1e6f5fee364761de673cb2116b4abc2e8b492af',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0051-step-runs',
|
||||
checksum:
|
||||
'3a659aff64ad2927a886b0e3f4309139a379f0a91f90ae147fd959d7db461a2a',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0052-capability-v26',
|
||||
checksum:
|
||||
'613ba0cddf1ed12ab00f4e3bf87e11cd14bac497c6502015496c58d909dfc5fc',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0053-tool-execution-evidence',
|
||||
checksum:
|
||||
'315cbdba8af417d38435d44317a1ada33a0b7b38a8a4f06ab08ba434a63e6a12',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0054-capability-v27',
|
||||
checksum:
|
||||
'8b6e33971669562b42ac7bd31689cbeff1247a71eea0b5533ab53cd8b2ec3551',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0055-tool-execution-start-barriers',
|
||||
checksum:
|
||||
'2209e9dbb301263ea79f7a3a08b61dc81586586c54eabed6c3911a10b81bc628',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0056-capability-v28',
|
||||
checksum:
|
||||
'58fe4e491d8c9e013fe94297e6a5d35591a524b19af468e9a7c1ae94750296a2',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0057-tool-invocation-artifacts',
|
||||
checksum:
|
||||
'430aeddb502e14ad750f56987ce7cfa8de8425e410ae859fae4b84b5120633a6',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0058-capability-v29',
|
||||
checksum:
|
||||
'e5cbd9874d5cc54aa4b99afcf26b3d514c329e00b32f09980d409c8d91fc164e',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0059-tool-execution-artifact-bindings',
|
||||
checksum:
|
||||
'0298421262b7b4b8accf4f9eba7619735399a6de8a4a11db51561f1fe9204c9d',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0060-capability-v30',
|
||||
checksum:
|
||||
'e05c92b3f01b2e38b71d18b63ba7d1932b971861d10c5d17b348ee1446b0efd9',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0061-tool-execution-completions',
|
||||
checksum:
|
||||
'b17111ce2f486a357931fe05d23fd7eccce3140793f1224ab59c20e922c615eb',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0062-capability-v31',
|
||||
checksum:
|
||||
'4e1aff95a4b573ca8147fbbdded001fb3a7dbbf657469eb08491a8e398cab367',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0063-tool-execution-failure-completions',
|
||||
checksum:
|
||||
'a6894b10361bbcf98f36e59cd79d8546f51ea45b1aa3798f20bfb54b00ea6712',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0064-capability-v32',
|
||||
checksum:
|
||||
'076978dabd6042cc04dc8ccb339a9eb788808bb12bcadd255b870f69fe5acc11',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0065-tool-result-key-catalog',
|
||||
checksum:
|
||||
'f7d241395920688e311dfc195bf051e3211e0ba7b39d5e97e0142df9cbb425d2',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0066-capability-v33',
|
||||
checksum:
|
||||
'ab5e64b4c4ae1fa23f8bb25cb34a83898d934f87744c4e0e98bdbbba678db669',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0067-tool-result-rekey-overlays',
|
||||
checksum:
|
||||
'b03d99ab33e53916890a1d2220038f7d3012162579a526e7c04aa54e775e7d89',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0068-capability-v34',
|
||||
checksum:
|
||||
'17d8a86261f4378f57efd3f134cc83d412a76a49a462380cd9c6976c326b635f',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0069-plugin-package-quarantine',
|
||||
checksum:
|
||||
'9e6055352f11c3d156551d3d4f5fb043e19193ed24a6517db2a0567e79cf3cf0',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0070-capability-v35',
|
||||
checksum:
|
||||
'bbca98db050d676174e23906cdeb065bdfa375d54673ed8ee898577c44694c82',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0071-local-identity-credential-administration',
|
||||
checksum:
|
||||
'94524df88e410a8c7f2b28832766f5dccc3d81e7075112819d51567c88896f77',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0072-capability-v36',
|
||||
checksum:
|
||||
'339b0d4ab576813aa37ee4b3347db68f415c01016c8fd8e0b293ae9e89ac1ba1',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0073-local-project-administration',
|
||||
checksum:
|
||||
'd8881e79284d4e63622ae7addc4d272c301290b83ca455ca642eb706c293093f',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0074-capability-v37',
|
||||
checksum:
|
||||
'3af70476c4a56b12dd208a3bee6cbe7da99788a7965e9691a36608afa88fa9ef',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0075-security-audit-compactions',
|
||||
checksum:
|
||||
'ccb424944e1392ed9a503d1d365fcaf6d0a4258b411eae0dc21da2f6fdf88b7e',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0076-capability-v38',
|
||||
checksum:
|
||||
'c73d5de0a6c846c606e2c345800f9fce570fced43cbe946e558f5ccc58a235b3',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0077-plugin-package-lifecycle',
|
||||
checksum:
|
||||
'9b308b31a53897e726f3e23a7ac3158754885d1d3aa487be5a7ec6a631fe1f71',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0078-capability-v39',
|
||||
checksum:
|
||||
'b9e26a65125bf43a40aee0a47eac65cf232411b084af2672ae06ef834dc17252',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0079-plugin-package-automation-publications',
|
||||
checksum:
|
||||
'6e4ede9a5b6c1480bba99bab3d257e5138cd8b3a7841d8fd5d6252dd264e78fd',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0080-capability-v40',
|
||||
checksum:
|
||||
'3cfd4dd22b3c0f399be3b3e6b1f85f65065c53a6b97361faa8c9ce5e0ec35702',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0081-plugin-package-workflow-admissions',
|
||||
checksum:
|
||||
'129e98a34ff88669218f31847b735a26afed9401a01d8384d71d04613adc51a3',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0082-capability-v41',
|
||||
checksum:
|
||||
'2ca9170340ef13d608e2f417df0a8a7df5252bf7a0dba81b1edc149a311e503c',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0083-plugin-package-workflow-task-attempt-admissions',
|
||||
checksum:
|
||||
'de30414a9667309960e99c6b232904e1fee769d6d319340ec8f40a36daeb213b',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0084-capability-v42',
|
||||
checksum:
|
||||
'0755449ac02f97e2b4074bd768e84d30fa62c9274d6f87370cff1b628fb0825e',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0085-plugin-package-workflow-run-list-index',
|
||||
checksum:
|
||||
'7f595bb1d0daf38a0f50594859ee475e5a377656b9ca086583ad1089911e1247',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0086-capability-v43',
|
||||
checksum:
|
||||
'd7affd7b3d1f3719dabc7abc7d5e8a2880fc4dc455b5103585befe9b51f705f9',
|
||||
}),
|
||||
]),
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
import type {
|
||||
MigrationStreamRecord,
|
||||
MigrationStreamStore,
|
||||
MigrationStreamTransaction,
|
||||
} from '@qinglong/runtime-core/migration-stream';
|
||||
import type { DatabaseSync } from 'node:sqlite';
|
||||
import type { LocalSqliteMigrationContext } from '../migrations/sqlMigration';
|
||||
|
||||
export const LOCAL_SQLITE_MIGRATION_STREAM_ID = 'ql3-local-sqlite';
|
||||
|
||||
interface HistoryRow {
|
||||
migration_id: unknown;
|
||||
stream_id: unknown;
|
||||
dialect: unknown;
|
||||
checksum: unknown;
|
||||
applied_at_ms: unknown;
|
||||
}
|
||||
|
||||
function record(row: HistoryRow): MigrationStreamRecord {
|
||||
if (
|
||||
typeof row.migration_id !== 'string' ||
|
||||
typeof row.stream_id !== 'string' ||
|
||||
row.dialect !== 'sqlite' ||
|
||||
typeof row.checksum !== 'string' ||
|
||||
typeof row.applied_at_ms !== 'number' ||
|
||||
!Number.isSafeInteger(row.applied_at_ms) ||
|
||||
row.applied_at_ms < 0
|
||||
) {
|
||||
throw new TypeError('Local SQLite migration history row is invalid');
|
||||
}
|
||||
return {
|
||||
migrationId: row.migration_id,
|
||||
streamId: row.stream_id,
|
||||
dialect: row.dialect,
|
||||
checksum: row.checksum,
|
||||
appliedAtMs: row.applied_at_ms,
|
||||
};
|
||||
}
|
||||
|
||||
export class LocalSqliteMigrationStreamStore
|
||||
implements MigrationStreamStore<LocalSqliteMigrationContext>
|
||||
{
|
||||
constructor(private readonly client: DatabaseSync) {}
|
||||
|
||||
async ensureHistory(): Promise<void> {
|
||||
this.client.exec(`
|
||||
CREATE TABLE IF NOT EXISTS "QingLong3SchemaMigrations" (
|
||||
migration_id TEXT PRIMARY KEY,
|
||||
stream_id TEXT NOT NULL,
|
||||
dialect TEXT NOT NULL
|
||||
CONSTRAINT ql3_local_migrations_dialect_check CHECK (dialect = 'sqlite'),
|
||||
checksum TEXT NOT NULL
|
||||
CONSTRAINT ql3_local_migrations_checksum_check
|
||||
CHECK (length(checksum) = 64 AND checksum NOT GLOB '*[^0-9a-f]*'),
|
||||
applied_at_ms INTEGER NOT NULL
|
||||
CONSTRAINT ql3_local_migrations_applied_at_check CHECK (applied_at_ms >= 0)
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
async listAll(): Promise<readonly MigrationStreamRecord[]> {
|
||||
return (
|
||||
this.client
|
||||
.prepare(
|
||||
`SELECT migration_id, stream_id, dialect, checksum, applied_at_ms
|
||||
FROM "QingLong3SchemaMigrations"
|
||||
ORDER BY migration_id`,
|
||||
)
|
||||
.all() as unknown as HistoryRow[]
|
||||
).map(record);
|
||||
}
|
||||
|
||||
async findById(
|
||||
migrationId: string,
|
||||
): Promise<MigrationStreamRecord | null> {
|
||||
const row = this.client
|
||||
.prepare(
|
||||
`SELECT migration_id, stream_id, dialect, checksum, applied_at_ms
|
||||
FROM "QingLong3SchemaMigrations"
|
||||
WHERE migration_id = ?`,
|
||||
)
|
||||
.get(migrationId) as HistoryRow | undefined;
|
||||
return row ? record(row) : null;
|
||||
}
|
||||
|
||||
async transaction<T>(
|
||||
work: (
|
||||
transaction: MigrationStreamTransaction<LocalSqliteMigrationContext>,
|
||||
) => Promise<T>,
|
||||
): Promise<T> {
|
||||
this.client.exec('BEGIN IMMEDIATE');
|
||||
try {
|
||||
const result = await work({
|
||||
context: { client: this.client },
|
||||
findById: (migrationId) => this.findById(migrationId),
|
||||
insert: async (value) => {
|
||||
if (
|
||||
value.streamId !== LOCAL_SQLITE_MIGRATION_STREAM_ID ||
|
||||
value.dialect !== 'sqlite'
|
||||
) {
|
||||
throw new TypeError('Local SQLite migration identity is invalid');
|
||||
}
|
||||
this.client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3SchemaMigrations"
|
||||
(migration_id, stream_id, dialect, checksum, applied_at_ms)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
value.migrationId,
|
||||
value.streamId,
|
||||
value.dialect,
|
||||
value.checksum,
|
||||
value.appliedAtMs,
|
||||
);
|
||||
},
|
||||
});
|
||||
this.client.exec('COMMIT');
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (this.client.isTransaction) this.client.exec('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0001RunCoreMigration = defineLocalSqliteMigration({
|
||||
id: '0001-run-core',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3SchemaCapabilities" (
|
||||
contract_name TEXT PRIMARY KEY,
|
||||
contract_version INTEGER NOT NULL
|
||||
CONSTRAINT ql3_local_capabilities_version_check CHECK (contract_version >= 1),
|
||||
migration_id TEXT NOT NULL,
|
||||
capabilities TEXT NOT NULL
|
||||
CONSTRAINT ql3_local_capabilities_json_check
|
||||
CHECK (json_valid(capabilities) AND json_type(capabilities) = 'object'),
|
||||
updated_at_ms INTEGER NOT NULL
|
||||
CONSTRAINT ql3_local_capabilities_updated_at_check CHECK (updated_at_ms >= 0),
|
||||
CONSTRAINT ql3_local_capabilities_migration_fk
|
||||
FOREIGN KEY (migration_id)
|
||||
REFERENCES "QingLong3SchemaMigrations" (migration_id)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE TABLE "Runs" (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL,
|
||||
task_id TEXT NOT NULL,
|
||||
task_revision TEXT NOT NULL,
|
||||
task_name TEXT,
|
||||
task_snapshot_ref TEXT,
|
||||
legacy_cron_id INTEGER,
|
||||
parent_run_id TEXT,
|
||||
retry_of_run_id TEXT,
|
||||
trigger_id TEXT,
|
||||
trigger_type TEXT NOT NULL,
|
||||
execution_origin TEXT NOT NULL
|
||||
CONSTRAINT ql3_local_runs_execution_origin_check
|
||||
CHECK (execution_origin IN ('manual','scheduled_system','scheduled_node','once','boot','grpc','subscription','system','script','legacy_import')),
|
||||
execution_owner TEXT NOT NULL
|
||||
CONSTRAINT ql3_local_runs_execution_owner_check
|
||||
CHECK (execution_owner IN ('legacy','runtime')),
|
||||
triggered_by TEXT,
|
||||
request_id TEXT,
|
||||
scheduled_for_ms INTEGER,
|
||||
status TEXT NOT NULL
|
||||
CONSTRAINT ql3_local_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_local_runs_version_check CHECK (version >= 0),
|
||||
event_sequence INTEGER NOT NULL DEFAULT 0
|
||||
CONSTRAINT ql3_local_runs_event_sequence_check CHECK (event_sequence >= 0),
|
||||
priority INTEGER NOT NULL DEFAULT 0,
|
||||
idempotency_key TEXT,
|
||||
input_ref TEXT,
|
||||
output_ref TEXT,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
queued_at_ms INTEGER,
|
||||
started_at_ms INTEGER,
|
||||
finished_at_ms INTEGER,
|
||||
cancel_requested_at_ms INTEGER,
|
||||
cancel_reason TEXT
|
||||
CONSTRAINT ql3_local_runs_cancel_reason_check
|
||||
CHECK (cancel_reason IS NULL OR cancel_reason IN ('user','policy','shutdown','reconcile','timeout')),
|
||||
error_code TEXT,
|
||||
error_summary TEXT,
|
||||
CONSTRAINT ql3_local_runs_time_check CHECK (
|
||||
created_at_ms >= 0 AND
|
||||
(scheduled_for_ms IS NULL OR scheduled_for_ms >= 0) AND
|
||||
(queued_at_ms IS NULL OR queued_at_ms >= 0) AND
|
||||
(started_at_ms IS NULL OR started_at_ms >= 0) AND
|
||||
(finished_at_ms IS NULL OR finished_at_ms >= 0) AND
|
||||
(cancel_requested_at_ms IS NULL OR cancel_requested_at_ms >= 0)
|
||||
),
|
||||
CONSTRAINT ql3_local_runs_parent_fk FOREIGN KEY (parent_run_id) REFERENCES "Runs" (id),
|
||||
CONSTRAINT ql3_local_runs_retry_fk FOREIGN KEY (retry_of_run_id) REFERENCES "Runs" (id)
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_local_runs_project_idempotency_uidx ON "Runs" (project_id, idempotency_key) WHERE idempotency_key IS NOT NULL`,
|
||||
`CREATE INDEX ql3_local_runs_project_created_idx ON "Runs" (project_id, created_at_ms, id)`,
|
||||
`CREATE INDEX ql3_local_runs_task_created_idx ON "Runs" (task_id, created_at_ms, id)`,
|
||||
`CREATE INDEX ql3_local_runs_cancel_requested_idx ON "Runs" (status, cancel_requested_at_ms, id)`,
|
||||
`CREATE INDEX ql3_local_runs_lost_retry_idx ON "Runs" (execution_owner, status, id)`,
|
||||
`
|
||||
CREATE TABLE "RunAttempts" (
|
||||
id TEXT PRIMARY KEY,
|
||||
run_id TEXT NOT NULL,
|
||||
step_run_id TEXT,
|
||||
attempt INTEGER NOT NULL
|
||||
CONSTRAINT ql3_local_attempts_attempt_check CHECK (attempt >= 1),
|
||||
status TEXT NOT NULL
|
||||
CONSTRAINT ql3_local_attempts_status_check
|
||||
CHECK (status IN ('claimed','starting','running','succeeded','failed','cancelled','timed_out','lost')),
|
||||
executor_type TEXT NOT NULL,
|
||||
worker_id TEXT,
|
||||
worker_session_id TEXT,
|
||||
worker_generation INTEGER,
|
||||
executor_handle TEXT,
|
||||
pid INTEGER,
|
||||
log_artifact_id TEXT,
|
||||
lease_token TEXT,
|
||||
lease_token_digest TEXT,
|
||||
lease_generation INTEGER,
|
||||
lease_version INTEGER,
|
||||
lease_expires_at_ms INTEGER,
|
||||
offer_id TEXT,
|
||||
deadline_at_ms INTEGER,
|
||||
callback_token_hash TEXT,
|
||||
callback_sequence INTEGER NOT NULL DEFAULT 0
|
||||
CONSTRAINT ql3_local_attempts_callback_sequence_check CHECK (callback_sequence >= 0),
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
started_at_ms INTEGER,
|
||||
finished_at_ms INTEGER,
|
||||
exit_code INTEGER,
|
||||
error_code TEXT,
|
||||
error_summary TEXT,
|
||||
CONSTRAINT ql3_local_attempts_time_check CHECK (
|
||||
created_at_ms >= 0 AND
|
||||
(started_at_ms IS NULL OR started_at_ms >= 0) AND
|
||||
(finished_at_ms IS NULL OR finished_at_ms >= 0) AND
|
||||
(lease_expires_at_ms IS NULL OR lease_expires_at_ms >= 0) AND
|
||||
(deadline_at_ms IS NULL OR deadline_at_ms >= 0)
|
||||
),
|
||||
CONSTRAINT ql3_local_attempts_run_fk FOREIGN KEY (run_id) REFERENCES "Runs" (id) ON DELETE CASCADE
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_local_attempts_run_attempt_uidx ON "RunAttempts" (run_id, attempt)`,
|
||||
`CREATE INDEX ql3_local_attempts_run_status_idx ON "RunAttempts" (run_id, status, id)`,
|
||||
`CREATE INDEX ql3_local_attempts_lease_idx ON "RunAttempts" (lease_expires_at_ms, id)`,
|
||||
`CREATE INDEX ql3_local_attempts_deadline_idx ON "RunAttempts" (status, deadline_at_ms, id)`,
|
||||
`
|
||||
CREATE TABLE "RunEvents" (
|
||||
id TEXT PRIMARY KEY,
|
||||
run_id TEXT NOT NULL,
|
||||
sequence INTEGER NOT NULL
|
||||
CONSTRAINT ql3_local_events_sequence_check CHECK (sequence >= 1),
|
||||
type TEXT NOT NULL,
|
||||
dedupe_key TEXT,
|
||||
actor_type TEXT NOT NULL
|
||||
CONSTRAINT ql3_local_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 TEXT,
|
||||
attempt_id TEXT,
|
||||
step_run_id TEXT,
|
||||
payload TEXT NOT NULL
|
||||
CONSTRAINT ql3_local_events_payload_check
|
||||
CHECK (json_valid(payload) AND json_type(payload) = 'object'),
|
||||
created_at_ms INTEGER NOT NULL
|
||||
CONSTRAINT ql3_local_events_created_at_check CHECK (created_at_ms >= 0),
|
||||
CONSTRAINT ql3_local_events_run_fk FOREIGN KEY (run_id) REFERENCES "Runs" (id) ON DELETE CASCADE,
|
||||
CONSTRAINT ql3_local_events_attempt_fk FOREIGN KEY (attempt_id) REFERENCES "RunAttempts" (id) ON DELETE SET NULL
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_local_events_run_sequence_uidx ON "RunEvents" (run_id, sequence)`,
|
||||
`CREATE UNIQUE INDEX ql3_local_events_run_dedupe_uidx ON "RunEvents" (run_id, dedupe_key) WHERE dedupe_key IS NOT NULL`,
|
||||
`CREATE INDEX ql3_local_events_run_created_idx ON "RunEvents" (run_id, created_at_ms, id)`,
|
||||
`
|
||||
CREATE TABLE "RunRetryPolicies" (
|
||||
run_id TEXT PRIMARY KEY,
|
||||
max_attempts INTEGER NOT NULL
|
||||
CONSTRAINT ql3_local_retry_max_attempts_check CHECK (max_attempts BETWEEN 1 AND 16),
|
||||
retry_on_lost INTEGER NOT NULL
|
||||
CONSTRAINT ql3_local_retry_on_lost_check CHECK (retry_on_lost IN (0, 1)),
|
||||
safety TEXT NOT NULL
|
||||
CONSTRAINT ql3_local_retry_safety_check CHECK (safety IN ('unknown','idempotent','deduplicated')),
|
||||
backoff_base_ms INTEGER NOT NULL,
|
||||
backoff_max_ms INTEGER NOT NULL,
|
||||
next_attempt_at_ms INTEGER,
|
||||
version INTEGER NOT NULL DEFAULT 0
|
||||
CONSTRAINT ql3_local_retry_version_check CHECK (version >= 0),
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
CONSTRAINT ql3_local_retry_backoff_check CHECK (
|
||||
backoff_base_ms BETWEEN 0 AND 86400000 AND
|
||||
backoff_max_ms BETWEEN backoff_base_ms AND 86400000
|
||||
),
|
||||
CONSTRAINT ql3_local_retry_time_check CHECK (
|
||||
created_at_ms >= 0 AND updated_at_ms >= created_at_ms AND
|
||||
(next_attempt_at_ms IS NULL OR next_attempt_at_ms >= 0)
|
||||
),
|
||||
CONSTRAINT ql3_local_retry_run_fk FOREIGN KEY (run_id) REFERENCES "Runs" (id) ON DELETE CASCADE
|
||||
)
|
||||
`,
|
||||
`CREATE INDEX ql3_local_retry_due_idx ON "RunRetryPolicies" (next_attempt_at_ms, run_id) WHERE next_attempt_at_ms IS NOT NULL`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0002CapabilityMigration = defineLocalSqliteMigration({
|
||||
id: '0002-capability',
|
||||
statements: [
|
||||
`
|
||||
INSERT INTO "QingLong3SchemaCapabilities" (
|
||||
contract_name, contract_version, migration_id, capabilities, updated_at_ms
|
||||
) VALUES (
|
||||
'local-control-core', 1, '0001-run-core', '{"run_core":1,"run_retry_policy":1}',
|
||||
CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
)
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0003CompletionReceiptJournalMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0003-completion-receipt-journal',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "LocalCompletionReceiptJournal" (
|
||||
attempt_id TEXT PRIMARY KEY,
|
||||
run_id TEXT NOT NULL,
|
||||
state TEXT NOT NULL
|
||||
CONSTRAINT ql3_local_receipt_journal_state_check
|
||||
CHECK (state IN ('pending','quarantined')),
|
||||
quarantine_ref TEXT,
|
||||
purge_after_ms INTEGER,
|
||||
registered_at_ms INTEGER NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
CONSTRAINT ql3_local_receipt_journal_time_check CHECK (
|
||||
registered_at_ms >= 0 AND updated_at_ms >= registered_at_ms AND
|
||||
(purge_after_ms IS NULL OR purge_after_ms >= updated_at_ms)
|
||||
),
|
||||
CONSTRAINT ql3_local_receipt_journal_shape_check CHECK (
|
||||
(state = 'pending' AND quarantine_ref IS NULL AND purge_after_ms IS NULL) OR
|
||||
(state = 'quarantined' AND quarantine_ref IS NOT NULL AND purge_after_ms IS NOT NULL)
|
||||
),
|
||||
CONSTRAINT ql3_local_receipt_journal_attempt_fk
|
||||
FOREIGN KEY (attempt_id) REFERENCES "RunAttempts" (id) ON DELETE CASCADE,
|
||||
CONSTRAINT ql3_local_receipt_journal_run_fk
|
||||
FOREIGN KEY (run_id) REFERENCES "Runs" (id) ON DELETE CASCADE
|
||||
)
|
||||
`,
|
||||
`CREATE INDEX ql3_local_receipt_journal_scan_idx ON "LocalCompletionReceiptJournal" (state, updated_at_ms, attempt_id)`,
|
||||
`CREATE INDEX ql3_local_receipt_journal_purge_idx ON "LocalCompletionReceiptJournal" (purge_after_ms, attempt_id) WHERE state = 'quarantined'`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0004CapabilityV2Migration = defineLocalSqliteMigration({
|
||||
id: '0004-capability-v2',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 2,
|
||||
migration_id = '0003-completion-receipt-journal',
|
||||
capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 1
|
||||
AND migration_id = '0001-run-core'
|
||||
AND capabilities = '{"run_core":1,"run_retry_policy":1}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0005LocalDispatchPlanMigration = defineLocalSqliteMigration({
|
||||
id: '0005-local-dispatch-plan',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3LocalExecutionContextRecipes" (
|
||||
"context_ref" TEXT PRIMARY KEY NOT NULL,
|
||||
"environment_json" TEXT NOT NULL,
|
||||
"content_digest" TEXT NOT NULL,
|
||||
"created_at_ms" INTEGER NOT NULL,
|
||||
CONSTRAINT ql3_local_context_ref_check CHECK (
|
||||
length("context_ref") = 80
|
||||
AND "context_ref" GLOB 'localctx:sha256:[0-9a-f]*'
|
||||
AND length(replace("context_ref", 'localctx:sha256:', '')) = 64
|
||||
),
|
||||
CONSTRAINT ql3_local_context_environment_check CHECK (
|
||||
json_valid("environment_json")
|
||||
AND json_type("environment_json") = 'array'
|
||||
AND length("environment_json") <= 262144
|
||||
),
|
||||
CONSTRAINT ql3_local_context_digest_check CHECK (
|
||||
length("content_digest") = 64
|
||||
AND "content_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
AND "context_ref" = 'localctx:sha256:' || "content_digest"
|
||||
),
|
||||
CONSTRAINT ql3_local_context_created_check CHECK ("created_at_ms" >= 0)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE TABLE "QingLong3LocalTaskExecutionRevisions" (
|
||||
"project_id" TEXT NOT NULL,
|
||||
"task_id" TEXT NOT NULL,
|
||||
"task_revision" TEXT NOT NULL,
|
||||
"executor_type" TEXT NOT NULL,
|
||||
"command_json" TEXT NOT NULL,
|
||||
"working_directory" TEXT,
|
||||
"timeout_ms" INTEGER,
|
||||
"context_ref" TEXT NOT NULL,
|
||||
"created_at_ms" INTEGER NOT NULL,
|
||||
PRIMARY KEY ("project_id", "task_id", "task_revision"),
|
||||
CONSTRAINT ql3_local_revision_executor_check CHECK (
|
||||
"executor_type" = 'local_process'
|
||||
),
|
||||
CONSTRAINT ql3_local_revision_command_check CHECK (
|
||||
json_valid("command_json")
|
||||
AND json_type("command_json") = 'object'
|
||||
AND length("command_json") BETWEEN 1 AND 131072
|
||||
),
|
||||
CONSTRAINT ql3_local_revision_working_directory_check CHECK (
|
||||
"working_directory" IS NULL
|
||||
OR (length("working_directory") BETWEEN 1 AND 4096
|
||||
AND substr("working_directory", 1, 1) = '/')
|
||||
),
|
||||
CONSTRAINT ql3_local_revision_timeout_check CHECK (
|
||||
"timeout_ms" IS NULL OR "timeout_ms" BETWEEN 1 AND 31536000000
|
||||
),
|
||||
CONSTRAINT ql3_local_revision_created_check CHECK ("created_at_ms" >= 0),
|
||||
FOREIGN KEY ("context_ref")
|
||||
REFERENCES "QingLong3LocalExecutionContextRecipes" ("context_ref")
|
||||
ON DELETE RESTRICT
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_local_runs_dispatch_idx"
|
||||
ON "Runs" (
|
||||
"execution_owner", "status", "priority" DESC,
|
||||
"queued_at_ms", "id"
|
||||
)
|
||||
WHERE "execution_owner" = 'runtime'
|
||||
AND "status" = 'queued'
|
||||
AND "cancel_requested_at_ms" IS NULL
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0006CapabilityV3Migration = defineLocalSqliteMigration({
|
||||
id: '0006-capability-v3',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 3,
|
||||
migration_id = '0005-local-dispatch-plan',
|
||||
capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 2
|
||||
AND migration_id = '0003-completion-receipt-journal'
|
||||
AND capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0007LocalSecretEnvelopesMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0007-local-secret-envelopes',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3LocalSecretEnvelopes" (
|
||||
"project_id" TEXT NOT NULL,
|
||||
"secret_name" TEXT NOT NULL,
|
||||
"version" INTEGER NOT NULL,
|
||||
"mutation_id" TEXT NOT NULL,
|
||||
"key_id" TEXT NOT NULL,
|
||||
"algorithm" TEXT NOT NULL,
|
||||
"nonce" BLOB NOT NULL,
|
||||
"ciphertext" BLOB NOT NULL,
|
||||
"auth_tag" BLOB NOT NULL,
|
||||
"created_at_ms" INTEGER NOT NULL,
|
||||
PRIMARY KEY ("project_id", "secret_name", "version"),
|
||||
CONSTRAINT ql3_local_secret_project_check CHECK (
|
||||
length("project_id") BETWEEN 1 AND 128
|
||||
AND "project_id" NOT GLOB '*[' || char(0) || '-' || char(31) || char(127) || ']*'
|
||||
),
|
||||
CONSTRAINT ql3_local_secret_name_check CHECK (
|
||||
length("secret_name") BETWEEN 1 AND 128
|
||||
AND "secret_name" NOT GLOB '*[' || char(0) || '-' || char(31) || char(127) || ']*'
|
||||
),
|
||||
CONSTRAINT ql3_local_secret_version_check CHECK (
|
||||
"version" BETWEEN 1 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_local_secret_mutation_check CHECK (
|
||||
length("mutation_id") BETWEEN 1 AND 64
|
||||
),
|
||||
CONSTRAINT ql3_local_secret_key_check CHECK (
|
||||
length("key_id") BETWEEN 1 AND 128
|
||||
AND "key_id" NOT GLOB '*[^A-Za-z0-9._-]*'
|
||||
),
|
||||
CONSTRAINT ql3_local_secret_algorithm_check CHECK (
|
||||
"algorithm" = 'aes-256-gcm'
|
||||
),
|
||||
CONSTRAINT ql3_local_secret_crypto_shape_check CHECK (
|
||||
length("nonce") = 12
|
||||
AND length("ciphertext") <= 16384
|
||||
AND length("auth_tag") = 16
|
||||
),
|
||||
CONSTRAINT ql3_local_secret_created_check CHECK ("created_at_ms" >= 0)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_local_secret_mutation_uidx"
|
||||
ON "QingLong3LocalSecretEnvelopes" (
|
||||
"project_id", "secret_name", "mutation_id"
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_local_secret_current_idx"
|
||||
ON "QingLong3LocalSecretEnvelopes" (
|
||||
"project_id", "secret_name", "version" DESC
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_local_secret_key_usage_idx"
|
||||
ON "QingLong3LocalSecretEnvelopes" (
|
||||
"key_id", "project_id", "secret_name", "version"
|
||||
)
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0008CapabilityV4Migration = defineLocalSqliteMigration({
|
||||
id: '0008-capability-v4',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 4,
|
||||
migration_id = '0007-local-secret-envelopes',
|
||||
capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 3
|
||||
AND migration_id = '0005-local-dispatch-plan'
|
||||
AND capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0009LocalProjectPolicyAuditMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0009-local-project-policy-audit',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3Projects" (
|
||||
"id" TEXT PRIMARY KEY NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL,
|
||||
"version" INTEGER NOT NULL,
|
||||
"created_at_ms" INTEGER NOT NULL,
|
||||
"updated_at_ms" INTEGER NOT NULL,
|
||||
CONSTRAINT ql3_local_projects_id_check CHECK (
|
||||
length("id") BETWEEN 1 AND 128
|
||||
AND "id" NOT GLOB '*[' || char(0) || '-' || char(31) || char(127) || ']*'
|
||||
),
|
||||
CONSTRAINT ql3_local_projects_name_check CHECK (
|
||||
length("name") BETWEEN 1 AND 255
|
||||
),
|
||||
CONSTRAINT ql3_local_projects_slug_check CHECK (
|
||||
length("slug") BETWEEN 1 AND 128
|
||||
AND "slug" = lower("slug")
|
||||
AND "slug" NOT GLOB '*[^a-z0-9-]*'
|
||||
AND substr("slug", 1, 1) NOT GLOB '[^a-z0-9]'
|
||||
AND substr("slug", -1, 1) NOT GLOB '[^a-z0-9]'
|
||||
),
|
||||
CONSTRAINT ql3_local_projects_status_check CHECK (
|
||||
"status" IN ('active', 'archived')
|
||||
),
|
||||
CONSTRAINT ql3_local_projects_version_check CHECK (
|
||||
"version" BETWEEN 1 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_local_projects_time_check CHECK (
|
||||
"created_at_ms" >= 0 AND "updated_at_ms" >= "created_at_ms"
|
||||
)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_local_projects_slug_uidx"
|
||||
ON "QingLong3Projects" ("slug")
|
||||
`,
|
||||
`
|
||||
INSERT INTO "QingLong3Projects" (
|
||||
"id", "name", "slug", "status", "version", "created_at_ms", "updated_at_ms"
|
||||
) VALUES ('default', 'Default', 'default', 'active', 1, 0, 0)
|
||||
`,
|
||||
`
|
||||
CREATE TABLE "QingLong3ProjectRoleBindings" (
|
||||
"project_id" TEXT NOT NULL,
|
||||
"subject_type" TEXT NOT NULL,
|
||||
"subject_id" TEXT NOT NULL,
|
||||
"version" INTEGER NOT NULL,
|
||||
"state" TEXT NOT NULL,
|
||||
"role" TEXT,
|
||||
"mutation_id" TEXT NOT NULL,
|
||||
"changed_by_type" TEXT NOT NULL,
|
||||
"changed_by_id" TEXT NOT NULL,
|
||||
"created_at_ms" INTEGER NOT NULL,
|
||||
PRIMARY KEY ("project_id", "subject_type", "subject_id", "version"),
|
||||
FOREIGN KEY ("project_id") REFERENCES "QingLong3Projects" ("id") ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_local_bindings_subject_type_check CHECK (
|
||||
"subject_type" IN ('user','api_app','mcp_client','agent','system','worker')
|
||||
),
|
||||
CONSTRAINT ql3_local_bindings_subject_id_check CHECK (
|
||||
length("subject_id") BETWEEN 1 AND 255
|
||||
),
|
||||
CONSTRAINT ql3_local_bindings_version_check CHECK (
|
||||
"version" BETWEEN 1 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_local_bindings_state_role_check CHECK (
|
||||
("state" = 'active' AND "role" IN ('owner','admin','operator','viewer'))
|
||||
OR ("state" = 'revoked' AND "role" IS NULL)
|
||||
),
|
||||
CONSTRAINT ql3_local_bindings_mutation_check CHECK (
|
||||
length("mutation_id") BETWEEN 1 AND 64
|
||||
),
|
||||
CONSTRAINT ql3_local_bindings_changed_by_type_check CHECK (
|
||||
"changed_by_type" IN ('user','api_app','mcp_client','agent','system','worker')
|
||||
),
|
||||
CONSTRAINT ql3_local_bindings_changed_by_id_check CHECK (
|
||||
length("changed_by_id") BETWEEN 1 AND 255
|
||||
),
|
||||
CONSTRAINT ql3_local_bindings_created_check CHECK ("created_at_ms" >= 0)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_local_bindings_mutation_uidx"
|
||||
ON "QingLong3ProjectRoleBindings" (
|
||||
"project_id", "subject_type", "subject_id", "mutation_id"
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_local_bindings_current_idx"
|
||||
ON "QingLong3ProjectRoleBindings" (
|
||||
"project_id", "subject_type", "subject_id", "version" DESC
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_local_bindings_project_idx"
|
||||
ON "QingLong3ProjectRoleBindings" ("project_id", "version" DESC)
|
||||
`,
|
||||
`
|
||||
CREATE TABLE "QingLong3SecurityAuditEvents" (
|
||||
"event_id" TEXT PRIMARY KEY NOT NULL,
|
||||
"request_id" TEXT NOT NULL,
|
||||
"operation_id" TEXT NOT NULL,
|
||||
"project_id" TEXT,
|
||||
"subject_type" TEXT,
|
||||
"subject_id" TEXT,
|
||||
"authentication_id" TEXT,
|
||||
"outcome" TEXT NOT NULL,
|
||||
"reasons_json" TEXT NOT NULL,
|
||||
"fence_project_version" INTEGER,
|
||||
"fence_binding_version" INTEGER,
|
||||
"occurred_at_ms" INTEGER NOT NULL,
|
||||
FOREIGN KEY ("project_id") REFERENCES "QingLong3Projects" ("id") ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_local_audit_event_check CHECK (length("event_id") = 36),
|
||||
CONSTRAINT ql3_local_audit_request_check CHECK (length("request_id") BETWEEN 1 AND 128),
|
||||
CONSTRAINT ql3_local_audit_operation_check CHECK (length("operation_id") BETWEEN 1 AND 128),
|
||||
CONSTRAINT ql3_local_audit_subject_check CHECK (
|
||||
("subject_type" IS NULL AND "subject_id" IS NULL AND "authentication_id" IS NULL)
|
||||
OR (
|
||||
"subject_type" IN ('user','api_app','mcp_client','agent','system','worker')
|
||||
AND length("subject_id") BETWEEN 1 AND 255
|
||||
AND length("authentication_id") BETWEEN 1 AND 128
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_local_audit_outcome_check CHECK (
|
||||
"outcome" IN (
|
||||
'authentication_rejected','authentication_unavailable',
|
||||
'authorization_unavailable','denied','approval_required','allowed'
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_local_audit_reasons_check CHECK (
|
||||
json_valid("reasons_json")
|
||||
AND json_type("reasons_json") = 'array'
|
||||
AND json_array_length("reasons_json") BETWEEN 1 AND 8
|
||||
AND length("reasons_json") <= 2048
|
||||
),
|
||||
CONSTRAINT ql3_local_audit_fence_check CHECK (
|
||||
("fence_project_version" IS NULL AND "fence_binding_version" IS NULL)
|
||||
OR (
|
||||
"fence_project_version" BETWEEN 1 AND 2147483647
|
||||
AND ("fence_binding_version" IS NULL OR "fence_binding_version" BETWEEN 1 AND 2147483647)
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_local_audit_time_check CHECK ("occurred_at_ms" >= 0)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_local_audit_project_time_idx"
|
||||
ON "QingLong3SecurityAuditEvents" (
|
||||
"project_id", "occurred_at_ms" DESC, "event_id" DESC
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_local_audit_subject_time_idx"
|
||||
ON "QingLong3SecurityAuditEvents" (
|
||||
"subject_type", "subject_id", "occurred_at_ms" DESC, "event_id" DESC
|
||||
)
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0010CapabilityV5Migration = defineLocalSqliteMigration({
|
||||
id: '0010-capability-v5',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 5,
|
||||
migration_id = '0009-local-project-policy-audit',
|
||||
capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 4
|
||||
AND migration_id = '0007-local-secret-envelopes'
|
||||
AND capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0011LocalIdentityCredentialMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0011-local-identity-credential',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3IdentitySubjects" (
|
||||
"subject_type" TEXT NOT NULL,
|
||||
"subject_id" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL,
|
||||
"version" INTEGER NOT NULL,
|
||||
"created_at_ms" INTEGER NOT NULL,
|
||||
"updated_at_ms" INTEGER NOT NULL,
|
||||
PRIMARY KEY ("subject_type", "subject_id"),
|
||||
CONSTRAINT ql3_local_identity_type_check CHECK (
|
||||
"subject_type" IN ('user','api_app','mcp_client','agent','system','worker')
|
||||
),
|
||||
CONSTRAINT ql3_local_identity_id_check CHECK (
|
||||
length("subject_id") BETWEEN 1 AND 255
|
||||
),
|
||||
CONSTRAINT ql3_local_identity_status_check CHECK (
|
||||
"status" IN ('active','disabled')
|
||||
),
|
||||
CONSTRAINT ql3_local_identity_version_check CHECK (
|
||||
"version" BETWEEN 1 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_local_identity_time_check CHECK (
|
||||
"created_at_ms" >= 0 AND "updated_at_ms" >= "created_at_ms"
|
||||
)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_local_identity_status_idx"
|
||||
ON "QingLong3IdentitySubjects" (
|
||||
"status", "subject_type", "subject_id"
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE TABLE "QingLong3ApiCredentials" (
|
||||
"credential_id" TEXT NOT NULL,
|
||||
"version" INTEGER NOT NULL,
|
||||
"state" TEXT NOT NULL,
|
||||
"subject_type" TEXT NOT NULL,
|
||||
"subject_id" TEXT NOT NULL,
|
||||
"secret_digest" TEXT NOT NULL,
|
||||
"created_at_ms" INTEGER NOT NULL,
|
||||
"not_before_at_ms" INTEGER NOT NULL,
|
||||
"expires_at_ms" INTEGER NOT NULL,
|
||||
PRIMARY KEY ("credential_id", "version"),
|
||||
FOREIGN KEY ("subject_type", "subject_id")
|
||||
REFERENCES "QingLong3IdentitySubjects" ("subject_type", "subject_id")
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_local_credentials_id_check CHECK (
|
||||
length("credential_id") BETWEEN 1 AND 64
|
||||
AND "credential_id" NOT GLOB '*[^A-Za-z0-9._:-]*'
|
||||
),
|
||||
CONSTRAINT ql3_local_credentials_version_check CHECK (
|
||||
"version" BETWEEN 1 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_local_credentials_state_check CHECK (
|
||||
"state" IN ('active','revoked')
|
||||
),
|
||||
CONSTRAINT ql3_local_credentials_subject_type_check CHECK (
|
||||
"subject_type" IN ('user','api_app','mcp_client','agent')
|
||||
),
|
||||
CONSTRAINT ql3_local_credentials_subject_id_check CHECK (
|
||||
length("subject_id") BETWEEN 1 AND 255
|
||||
),
|
||||
CONSTRAINT ql3_local_credentials_digest_check CHECK (
|
||||
length("secret_digest") = 64
|
||||
AND "secret_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_local_credentials_lifetime_check CHECK (
|
||||
"created_at_ms" >= 0
|
||||
AND "not_before_at_ms" >= "created_at_ms"
|
||||
AND "expires_at_ms" > "not_before_at_ms"
|
||||
)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_local_credentials_current_idx"
|
||||
ON "QingLong3ApiCredentials" ("credential_id", "version" DESC)
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_local_credentials_subject_idx"
|
||||
ON "QingLong3ApiCredentials" (
|
||||
"subject_type", "subject_id", "credential_id", "version" DESC
|
||||
)
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0012CapabilityV6Migration = defineLocalSqliteMigration({
|
||||
id: '0012-capability-v6',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 6,
|
||||
migration_id = '0011-local-identity-credential',
|
||||
capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 5
|
||||
AND migration_id = '0009-local-project-policy-audit'
|
||||
AND capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0013LocalOwnerBootstrapMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0013-local-owner-bootstrap',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3LocalIdentityProvisionings" (
|
||||
"slot" INTEGER PRIMARY KEY NOT NULL,
|
||||
"mutation_id" TEXT NOT NULL,
|
||||
"request_id" TEXT NOT NULL,
|
||||
"subject_type" TEXT NOT NULL,
|
||||
"subject_id" TEXT NOT NULL,
|
||||
"credential_id" TEXT NOT NULL,
|
||||
"credential_version" INTEGER NOT NULL,
|
||||
"issuer_authentication_id" TEXT NOT NULL,
|
||||
"issuer_authenticated_at_ms" INTEGER NOT NULL,
|
||||
"issuer_expires_at_ms" INTEGER NOT NULL,
|
||||
"audit_event_id" TEXT NOT NULL,
|
||||
"created_at_ms" INTEGER NOT NULL,
|
||||
FOREIGN KEY ("subject_type", "subject_id")
|
||||
REFERENCES "QingLong3IdentitySubjects" ("subject_type", "subject_id")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("credential_id", "credential_version")
|
||||
REFERENCES "QingLong3ApiCredentials" ("credential_id", "version")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("audit_event_id")
|
||||
REFERENCES "QingLong3SecurityAuditEvents" ("event_id")
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_local_provisioning_singleton_check CHECK ("slot" = 1),
|
||||
CONSTRAINT ql3_local_provisioning_mutation_check CHECK (
|
||||
length("mutation_id") = 36
|
||||
),
|
||||
CONSTRAINT ql3_local_provisioning_request_check CHECK (
|
||||
length("request_id") BETWEEN 1 AND 128
|
||||
),
|
||||
CONSTRAINT ql3_local_provisioning_subject_check CHECK (
|
||||
"subject_type" = 'user' AND length("subject_id") BETWEEN 1 AND 255
|
||||
),
|
||||
CONSTRAINT ql3_local_provisioning_credential_check CHECK (
|
||||
length("credential_id") BETWEEN 1 AND 64 AND "credential_version" = 1
|
||||
),
|
||||
CONSTRAINT ql3_local_provisioning_issuer_check CHECK (
|
||||
length("issuer_authentication_id") BETWEEN 1 AND 128
|
||||
AND "issuer_authenticated_at_ms" <= "created_at_ms"
|
||||
AND "issuer_expires_at_ms" > "created_at_ms"
|
||||
),
|
||||
CONSTRAINT ql3_local_provisioning_audit_check CHECK (
|
||||
"audit_event_id" = "mutation_id"
|
||||
),
|
||||
CONSTRAINT ql3_local_provisioning_time_check CHECK ("created_at_ms" >= 0)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_local_provisioning_mutation_uidx"
|
||||
ON "QingLong3LocalIdentityProvisionings" ("mutation_id")
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_local_provisioning_subject_uidx"
|
||||
ON "QingLong3LocalIdentityProvisionings" ("subject_type", "subject_id")
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_local_provisioning_credential_uidx"
|
||||
ON "QingLong3LocalIdentityProvisionings" (
|
||||
"credential_id", "credential_version"
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE TABLE "QingLong3LocalOwnerBootstrapChallenges" (
|
||||
"project_id" TEXT NOT NULL,
|
||||
"version" INTEGER NOT NULL,
|
||||
"issue_mutation_id" TEXT NOT NULL,
|
||||
"issue_request_id" TEXT NOT NULL,
|
||||
"challenge_id" TEXT NOT NULL,
|
||||
"token_digest" TEXT NOT NULL,
|
||||
"issuer_authentication_id" TEXT NOT NULL,
|
||||
"issuer_authenticated_at_ms" INTEGER NOT NULL,
|
||||
"issuer_expires_at_ms" INTEGER NOT NULL,
|
||||
"issued_at_ms" INTEGER NOT NULL,
|
||||
"expires_at_ms" INTEGER NOT NULL,
|
||||
"issue_audit_event_id" TEXT NOT NULL,
|
||||
"consumed_at_ms" INTEGER,
|
||||
"claim_mutation_id" TEXT,
|
||||
"claim_request_id" TEXT,
|
||||
"claimed_subject_type" TEXT,
|
||||
"claimed_subject_id" TEXT,
|
||||
"credential_id" TEXT,
|
||||
"credential_version" INTEGER,
|
||||
"claim_authentication_id" TEXT,
|
||||
"claim_authenticated_at_ms" INTEGER,
|
||||
"claim_expires_at_ms" INTEGER,
|
||||
"claim_assurance" TEXT,
|
||||
"claim_audit_event_id" TEXT,
|
||||
PRIMARY KEY ("project_id", "version"),
|
||||
FOREIGN KEY ("project_id")
|
||||
REFERENCES "QingLong3Projects" ("id") ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("issue_audit_event_id")
|
||||
REFERENCES "QingLong3SecurityAuditEvents" ("event_id") ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("claimed_subject_type", "claimed_subject_id")
|
||||
REFERENCES "QingLong3IdentitySubjects" ("subject_type", "subject_id")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("credential_id", "credential_version")
|
||||
REFERENCES "QingLong3ApiCredentials" ("credential_id", "version")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("claim_audit_event_id")
|
||||
REFERENCES "QingLong3SecurityAuditEvents" ("event_id") ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_local_owner_challenge_version_check CHECK (
|
||||
"version" BETWEEN 1 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_challenge_issue_identity_check CHECK (
|
||||
length("issue_mutation_id") = 36
|
||||
AND length("issue_request_id") BETWEEN 1 AND 128
|
||||
AND "issue_audit_event_id" = "issue_mutation_id"
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_challenge_id_check CHECK (
|
||||
length("challenge_id") = 22
|
||||
AND "challenge_id" NOT GLOB '*[^A-Za-z0-9_-]*'
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_challenge_digest_check CHECK (
|
||||
length("token_digest") = 64
|
||||
AND "token_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_challenge_issuer_check CHECK (
|
||||
length("issuer_authentication_id") BETWEEN 1 AND 128
|
||||
AND "issuer_authenticated_at_ms" <= "issued_at_ms"
|
||||
AND "issuer_expires_at_ms" > "issued_at_ms"
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_challenge_lifetime_check CHECK (
|
||||
"issued_at_ms" >= 0
|
||||
AND "expires_at_ms" > "issued_at_ms"
|
||||
AND "expires_at_ms" - "issued_at_ms" BETWEEN 60000 AND 1800000
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_challenge_claim_shape_check CHECK (
|
||||
(
|
||||
"consumed_at_ms" IS NULL
|
||||
AND "claim_mutation_id" IS NULL
|
||||
AND "claim_request_id" IS NULL
|
||||
AND "claimed_subject_type" IS NULL
|
||||
AND "claimed_subject_id" IS NULL
|
||||
AND "credential_id" IS NULL
|
||||
AND "credential_version" IS NULL
|
||||
AND "claim_authentication_id" IS NULL
|
||||
AND "claim_authenticated_at_ms" IS NULL
|
||||
AND "claim_expires_at_ms" IS NULL
|
||||
AND "claim_assurance" IS NULL
|
||||
AND "claim_audit_event_id" IS NULL
|
||||
)
|
||||
OR (
|
||||
"consumed_at_ms" >= "issued_at_ms"
|
||||
AND "consumed_at_ms" < "expires_at_ms"
|
||||
AND length("claim_mutation_id") = 36
|
||||
AND length("claim_request_id") BETWEEN 1 AND 128
|
||||
AND "claimed_subject_type" = 'user'
|
||||
AND length("claimed_subject_id") BETWEEN 1 AND 255
|
||||
AND length("credential_id") BETWEEN 1 AND 64
|
||||
AND "credential_version" BETWEEN 1 AND 2147483647
|
||||
AND length("claim_authentication_id") BETWEEN 1 AND 128
|
||||
AND "claim_authenticated_at_ms" <= "consumed_at_ms"
|
||||
AND "claim_expires_at_ms" > "consumed_at_ms"
|
||||
AND "claim_assurance" = 'single_factor'
|
||||
AND "claim_audit_event_id" = "claim_mutation_id"
|
||||
)
|
||||
)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_local_owner_challenge_issue_mutation_uidx"
|
||||
ON "QingLong3LocalOwnerBootstrapChallenges" ("issue_mutation_id")
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_local_owner_challenge_id_uidx"
|
||||
ON "QingLong3LocalOwnerBootstrapChallenges" ("challenge_id")
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_local_owner_challenge_claim_mutation_uidx"
|
||||
ON "QingLong3LocalOwnerBootstrapChallenges" ("claim_mutation_id")
|
||||
WHERE "claim_mutation_id" IS NOT NULL
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_local_owner_challenge_current_idx"
|
||||
ON "QingLong3LocalOwnerBootstrapChallenges" (
|
||||
"project_id", "version" DESC
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_local_owner_challenge_expiry_idx"
|
||||
ON "QingLong3LocalOwnerBootstrapChallenges" (
|
||||
"project_id", "expires_at_ms", "version" DESC
|
||||
)
|
||||
WHERE "consumed_at_ms" IS NULL
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0014CapabilityV7Migration = defineLocalSqliteMigration({
|
||||
id: '0014-capability-v7',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 7,
|
||||
migration_id = '0013-local-owner-bootstrap',
|
||||
capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 6
|
||||
AND migration_id = '0011-local-identity-credential'
|
||||
AND capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0015LocalOwnerDeliveryAcknowledgementsMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0015-local-owner-delivery-acknowledgements',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3LocalOwnerDeliveryAcknowledgements" (
|
||||
"mutation_id" TEXT PRIMARY KEY NOT NULL,
|
||||
"kind" TEXT NOT NULL,
|
||||
"request_id" TEXT NOT NULL,
|
||||
"project_id" TEXT,
|
||||
"subject_id" TEXT,
|
||||
"credential_id" TEXT,
|
||||
"challenge_id" TEXT,
|
||||
"fact_digest" TEXT NOT NULL,
|
||||
"delivery_digest" TEXT NOT NULL,
|
||||
"ttl_ms" INTEGER NOT NULL,
|
||||
"acknowledged_at_ms" INTEGER NOT NULL,
|
||||
"provisioning_mutation_id" TEXT,
|
||||
"challenge_mutation_id" TEXT,
|
||||
FOREIGN KEY ("provisioning_mutation_id")
|
||||
REFERENCES "QingLong3LocalIdentityProvisionings" ("mutation_id")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("challenge_mutation_id")
|
||||
REFERENCES "QingLong3LocalOwnerBootstrapChallenges" ("issue_mutation_id")
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_local_owner_delivery_ack_mutation_check CHECK (
|
||||
length("mutation_id") = 36
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_delivery_ack_request_check CHECK (
|
||||
length("request_id") BETWEEN 1 AND 128
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_delivery_ack_digest_check CHECK (
|
||||
length("fact_digest") = 64
|
||||
AND "fact_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
AND length("delivery_digest") = 64
|
||||
AND "delivery_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_delivery_ack_time_check CHECK (
|
||||
"ttl_ms" > 0 AND "acknowledged_at_ms" >= 0
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_delivery_ack_shape_check CHECK (
|
||||
(
|
||||
"kind" = 'credential'
|
||||
AND "project_id" IS NULL
|
||||
AND length("subject_id") = 26
|
||||
AND "subject_id" GLOB 'usr_*'
|
||||
AND "subject_id" NOT GLOB '*[^A-Za-z0-9_-]*'
|
||||
AND length("credential_id") = 26
|
||||
AND "credential_id" GLOB 'own_*'
|
||||
AND "credential_id" NOT GLOB '*[^A-Za-z0-9_-]*'
|
||||
AND "challenge_id" IS NULL
|
||||
AND "provisioning_mutation_id" = "mutation_id"
|
||||
AND "challenge_mutation_id" IS NULL
|
||||
AND "ttl_ms" BETWEEN 600000 AND 604800000
|
||||
)
|
||||
OR (
|
||||
"kind" = 'challenge'
|
||||
AND length("project_id") BETWEEN 1 AND 128
|
||||
AND "subject_id" IS NULL
|
||||
AND "credential_id" IS NULL
|
||||
AND length("challenge_id") = 22
|
||||
AND "challenge_id" NOT GLOB '*[^A-Za-z0-9_-]*'
|
||||
AND "provisioning_mutation_id" IS NULL
|
||||
AND "challenge_mutation_id" = "mutation_id"
|
||||
AND "ttl_ms" BETWEEN 60000 AND 1800000
|
||||
)
|
||||
)
|
||||
)
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0016CapabilityV8Migration = defineLocalSqliteMigration({
|
||||
id: '0016-capability-v8',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 8,
|
||||
migration_id = '0015-local-owner-delivery-acknowledgements',
|
||||
capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 7
|
||||
AND migration_id = '0013-local-owner-bootstrap'
|
||||
AND capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0017ApiCredentialPepperBindingsMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0017-api-credential-pepper-bindings',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3ApiCredentialPepperBindings" (
|
||||
"credential_id" TEXT NOT NULL,
|
||||
"credential_version" INTEGER NOT NULL,
|
||||
"pepper_key_id" TEXT NOT NULL,
|
||||
PRIMARY KEY ("credential_id", "credential_version"),
|
||||
FOREIGN KEY ("credential_id", "credential_version")
|
||||
REFERENCES "QingLong3ApiCredentials" ("credential_id", "version")
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_local_credential_pepper_key_id_check CHECK (
|
||||
length("pepper_key_id") BETWEEN 1 AND 64
|
||||
AND "pepper_key_id" NOT GLOB '*[^A-Za-z0-9._:-]*'
|
||||
)
|
||||
)
|
||||
`,
|
||||
`
|
||||
INSERT INTO "QingLong3ApiCredentialPepperBindings" (
|
||||
"credential_id", "credential_version", "pepper_key_id"
|
||||
)
|
||||
SELECT "credential_id", "version", 'legacy-v1'
|
||||
FROM "QingLong3ApiCredentials"
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_local_credential_pepper_key_idx"
|
||||
ON "QingLong3ApiCredentialPepperBindings" (
|
||||
"pepper_key_id", "credential_id", "credential_version"
|
||||
)
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0018CapabilityV9Migration = defineLocalSqliteMigration({
|
||||
id: '0018-capability-v9',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 9,
|
||||
migration_id = '0017-api-credential-pepper-bindings',
|
||||
capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 8
|
||||
AND migration_id = '0015-local-owner-delivery-acknowledgements'
|
||||
AND capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,182 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0019LocalOwnerPepperCatalogMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0019-local-owner-pepper-catalog',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3LocalOwnerPepperKeys" (
|
||||
"pepper_key_id" TEXT PRIMARY KEY,
|
||||
"material_digest" TEXT,
|
||||
"backup_digest" TEXT,
|
||||
"state" TEXT NOT NULL,
|
||||
"version" INTEGER NOT NULL,
|
||||
"register_mutation_id" TEXT,
|
||||
"activate_mutation_id" TEXT,
|
||||
"retire_mutation_id" TEXT,
|
||||
"registered_at_ms" INTEGER NOT NULL,
|
||||
"activated_at_ms" INTEGER,
|
||||
"retired_at_ms" INTEGER,
|
||||
CONSTRAINT ql3_local_owner_pepper_key_id_check CHECK (
|
||||
length("pepper_key_id") BETWEEN 1 AND 64
|
||||
AND "pepper_key_id" NOT GLOB '*[^A-Za-z0-9._:-]*'
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_pepper_digest_check CHECK (
|
||||
("material_digest" IS NULL AND "backup_digest" IS NULL)
|
||||
OR (
|
||||
length("material_digest") = 64
|
||||
AND "material_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
AND length("backup_digest") = 64
|
||||
AND "backup_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_pepper_state_check CHECK (
|
||||
"state" IN ('recovery_required', 'staged', 'active', 'retired')
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_pepper_version_check CHECK (
|
||||
"version" BETWEEN 1 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_pepper_mutation_check CHECK (
|
||||
("register_mutation_id" IS NULL OR length("register_mutation_id") = 36)
|
||||
AND ("activate_mutation_id" IS NULL OR length("activate_mutation_id") = 36)
|
||||
AND ("retire_mutation_id" IS NULL OR length("retire_mutation_id") = 36)
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_pepper_time_check CHECK (
|
||||
"registered_at_ms" >= 0
|
||||
AND ("activated_at_ms" IS NULL OR "activated_at_ms" >= "registered_at_ms")
|
||||
AND ("retired_at_ms" IS NULL OR "retired_at_ms" >= "activated_at_ms")
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_pepper_shape_check CHECK (
|
||||
("state" = 'recovery_required' AND "material_digest" IS NULL
|
||||
AND "backup_digest" IS NULL AND "register_mutation_id" IS NULL
|
||||
AND "activate_mutation_id" IS NULL AND "retire_mutation_id" IS NULL
|
||||
AND "activated_at_ms" IS NULL AND "retired_at_ms" IS NULL)
|
||||
OR ("state" = 'staged' AND "material_digest" IS NOT NULL
|
||||
AND "backup_digest" IS NOT NULL AND "register_mutation_id" IS NOT NULL
|
||||
AND "activate_mutation_id" IS NULL AND "retire_mutation_id" IS NULL
|
||||
AND "activated_at_ms" IS NULL AND "retired_at_ms" IS NULL)
|
||||
OR ("state" = 'active' AND "material_digest" IS NOT NULL
|
||||
AND "backup_digest" IS NOT NULL AND "register_mutation_id" IS NOT NULL
|
||||
AND "activate_mutation_id" IS NOT NULL AND "retire_mutation_id" IS NULL
|
||||
AND "activated_at_ms" IS NOT NULL AND "retired_at_ms" IS NULL)
|
||||
OR ("state" = 'retired' AND "material_digest" IS NOT NULL
|
||||
AND "backup_digest" IS NOT NULL AND "register_mutation_id" IS NOT NULL
|
||||
AND "activate_mutation_id" IS NOT NULL AND "retire_mutation_id" IS NOT NULL
|
||||
AND "activated_at_ms" IS NOT NULL AND "retired_at_ms" IS NOT NULL)
|
||||
)
|
||||
)
|
||||
`,
|
||||
`
|
||||
INSERT INTO "QingLong3LocalOwnerPepperKeys" (
|
||||
"pepper_key_id", "state", "version", "registered_at_ms"
|
||||
)
|
||||
SELECT 'legacy-v1', 'recovery_required', 1, 0
|
||||
WHERE EXISTS (SELECT 1 FROM "QingLong3ApiCredentialPepperBindings" LIMIT 1)
|
||||
`,
|
||||
`
|
||||
ALTER TABLE "QingLong3ApiCredentialPepperBindings"
|
||||
RENAME TO "QingLong3ApiCredentialPepperBindingsBeforeCatalog"
|
||||
`,
|
||||
`
|
||||
CREATE TABLE "QingLong3ApiCredentialPepperBindings" (
|
||||
"credential_id" TEXT NOT NULL,
|
||||
"credential_version" INTEGER NOT NULL,
|
||||
"pepper_key_id" TEXT NOT NULL,
|
||||
PRIMARY KEY ("credential_id", "credential_version"),
|
||||
FOREIGN KEY ("credential_id", "credential_version")
|
||||
REFERENCES "QingLong3ApiCredentials" ("credential_id", "version")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("pepper_key_id")
|
||||
REFERENCES "QingLong3LocalOwnerPepperKeys" ("pepper_key_id")
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_local_credential_pepper_key_id_check CHECK (
|
||||
length("pepper_key_id") BETWEEN 1 AND 64
|
||||
AND "pepper_key_id" NOT GLOB '*[^A-Za-z0-9._:-]*'
|
||||
)
|
||||
)
|
||||
`,
|
||||
`
|
||||
INSERT INTO "QingLong3ApiCredentialPepperBindings" (
|
||||
"credential_id", "credential_version", "pepper_key_id"
|
||||
)
|
||||
SELECT "credential_id", "credential_version", "pepper_key_id"
|
||||
FROM "QingLong3ApiCredentialPepperBindingsBeforeCatalog"
|
||||
`,
|
||||
`
|
||||
DROP TABLE "QingLong3ApiCredentialPepperBindingsBeforeCatalog"
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_local_credential_pepper_key_idx"
|
||||
ON "QingLong3ApiCredentialPepperBindings" (
|
||||
"pepper_key_id", "credential_id", "credential_version"
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_local_owner_pepper_register_mutation_uidx"
|
||||
ON "QingLong3LocalOwnerPepperKeys" ("register_mutation_id")
|
||||
WHERE "register_mutation_id" IS NOT NULL
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_local_owner_pepper_single_active_uidx"
|
||||
ON "QingLong3LocalOwnerPepperKeys" ("state")
|
||||
WHERE "state" = 'active'
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_local_owner_pepper_state_idx"
|
||||
ON "QingLong3LocalOwnerPepperKeys" ("state", "pepper_key_id")
|
||||
`,
|
||||
`
|
||||
CREATE TABLE "QingLong3LocalOwnerPepperActivations" (
|
||||
"generation" INTEGER PRIMARY KEY,
|
||||
"mutation_id" TEXT NOT NULL,
|
||||
"expected_generation" INTEGER NOT NULL,
|
||||
"previous_pepper_key_id" TEXT,
|
||||
"active_pepper_key_id" TEXT NOT NULL,
|
||||
"material_digest" TEXT NOT NULL,
|
||||
"backup_digest" TEXT NOT NULL,
|
||||
"activated_at_ms" INTEGER NOT NULL,
|
||||
CONSTRAINT ql3_local_owner_pepper_activation_generation_check CHECK (
|
||||
"generation" BETWEEN 1 AND 2147483647
|
||||
AND "expected_generation" = "generation" - 1
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_pepper_activation_mutation_check CHECK (
|
||||
length("mutation_id") = 36
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_pepper_activation_key_check CHECK (
|
||||
length("active_pepper_key_id") BETWEEN 1 AND 64
|
||||
AND "active_pepper_key_id" NOT GLOB '*[^A-Za-z0-9._:-]*'
|
||||
AND ("previous_pepper_key_id" IS NULL OR (
|
||||
length("previous_pepper_key_id") BETWEEN 1 AND 64
|
||||
AND "previous_pepper_key_id" NOT GLOB '*[^A-Za-z0-9._:-]*'
|
||||
AND "previous_pepper_key_id" <> "active_pepper_key_id"
|
||||
))
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_pepper_activation_digest_check CHECK (
|
||||
length("material_digest") = 64
|
||||
AND "material_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
AND length("backup_digest") = 64
|
||||
AND "backup_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_pepper_activation_time_check CHECK (
|
||||
"activated_at_ms" >= 0
|
||||
),
|
||||
FOREIGN KEY ("previous_pepper_key_id")
|
||||
REFERENCES "QingLong3LocalOwnerPepperKeys" ("pepper_key_id")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("active_pepper_key_id")
|
||||
REFERENCES "QingLong3LocalOwnerPepperKeys" ("pepper_key_id")
|
||||
ON DELETE RESTRICT
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_local_owner_pepper_activation_mutation_uidx"
|
||||
ON "QingLong3LocalOwnerPepperActivations" ("mutation_id")
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_local_owner_pepper_activation_key_idx"
|
||||
ON "QingLong3LocalOwnerPepperActivations" (
|
||||
"active_pepper_key_id", "generation" DESC
|
||||
)
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0020CapabilityV10Migration = defineLocalSqliteMigration({
|
||||
id: '0020-capability-v10',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 10,
|
||||
migration_id = '0019-local-owner-pepper-catalog',
|
||||
capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 9
|
||||
AND migration_id = '0017-api-credential-pepper-bindings'
|
||||
AND capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0021LocalOwnerCredentialRecoveryMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0021-local-owner-credential-recovery',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3LocalOwnerCredentialRecoveries" (
|
||||
"issue_mutation_id" TEXT PRIMARY KEY NOT NULL,
|
||||
"issue_request_id" TEXT NOT NULL,
|
||||
"subject_type" TEXT NOT NULL,
|
||||
"subject_id" TEXT NOT NULL,
|
||||
"previous_credential_id" TEXT NOT NULL,
|
||||
"previous_credential_version" INTEGER NOT NULL,
|
||||
"replacement_credential_id" TEXT NOT NULL,
|
||||
"replacement_credential_version" INTEGER NOT NULL,
|
||||
"state" TEXT NOT NULL,
|
||||
"issued_at_ms" INTEGER NOT NULL,
|
||||
"issue_audit_event_id" TEXT NOT NULL,
|
||||
"delivery_digest" TEXT,
|
||||
"acknowledged_at_ms" INTEGER,
|
||||
"complete_mutation_id" TEXT,
|
||||
"complete_request_id" TEXT,
|
||||
"revoked_credential_version" INTEGER,
|
||||
"completed_at_ms" INTEGER,
|
||||
"complete_audit_event_id" TEXT,
|
||||
FOREIGN KEY ("subject_type", "subject_id")
|
||||
REFERENCES "QingLong3IdentitySubjects" ("subject_type", "subject_id")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("previous_credential_id", "previous_credential_version")
|
||||
REFERENCES "QingLong3ApiCredentials" ("credential_id", "version")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("replacement_credential_id", "replacement_credential_version")
|
||||
REFERENCES "QingLong3ApiCredentials" ("credential_id", "version")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("previous_credential_id", "revoked_credential_version")
|
||||
REFERENCES "QingLong3ApiCredentials" ("credential_id", "version")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("issue_audit_event_id")
|
||||
REFERENCES "QingLong3SecurityAuditEvents" ("event_id")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("complete_audit_event_id")
|
||||
REFERENCES "QingLong3SecurityAuditEvents" ("event_id")
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_local_owner_recovery_mutation_check CHECK (
|
||||
length("issue_mutation_id") = 36
|
||||
AND "issue_audit_event_id" = "issue_mutation_id"
|
||||
AND ("complete_mutation_id" IS NULL OR (
|
||||
length("complete_mutation_id") = 36
|
||||
AND "complete_audit_event_id" = "complete_mutation_id"
|
||||
AND "complete_mutation_id" <> "issue_mutation_id"
|
||||
))
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_recovery_request_check CHECK (
|
||||
length("issue_request_id") BETWEEN 1 AND 128
|
||||
AND ("complete_request_id" IS NULL
|
||||
OR length("complete_request_id") BETWEEN 1 AND 128)
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_recovery_identity_check CHECK (
|
||||
"subject_type" = 'user'
|
||||
AND length("subject_id") = 26
|
||||
AND "subject_id" GLOB 'usr_*'
|
||||
AND "subject_id" NOT GLOB '*[^A-Za-z0-9_-]*'
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_recovery_credential_check CHECK (
|
||||
length("previous_credential_id") BETWEEN 1 AND 64
|
||||
AND "previous_credential_id" NOT GLOB '*[^A-Za-z0-9._:-]*'
|
||||
AND "previous_credential_version" BETWEEN 1 AND 2147483646
|
||||
AND length("replacement_credential_id") BETWEEN 1 AND 64
|
||||
AND "replacement_credential_id" NOT GLOB '*[^A-Za-z0-9._:-]*'
|
||||
AND "replacement_credential_id" <> "previous_credential_id"
|
||||
AND "replacement_credential_version" = 1
|
||||
AND ("revoked_credential_version" IS NULL
|
||||
OR "revoked_credential_version" = "previous_credential_version" + 1)
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_recovery_digest_check CHECK (
|
||||
"delivery_digest" IS NULL OR (
|
||||
length("delivery_digest") = 64
|
||||
AND "delivery_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_recovery_time_check CHECK (
|
||||
"issued_at_ms" >= 0
|
||||
AND ("acknowledged_at_ms" IS NULL
|
||||
OR "acknowledged_at_ms" >= "issued_at_ms")
|
||||
AND ("completed_at_ms" IS NULL
|
||||
OR "completed_at_ms" >= "acknowledged_at_ms")
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_recovery_shape_check CHECK (
|
||||
("state" = 'issued'
|
||||
AND "delivery_digest" IS NULL
|
||||
AND "acknowledged_at_ms" IS NULL
|
||||
AND "complete_mutation_id" IS NULL
|
||||
AND "complete_request_id" IS NULL
|
||||
AND "revoked_credential_version" IS NULL
|
||||
AND "completed_at_ms" IS NULL
|
||||
AND "complete_audit_event_id" IS NULL)
|
||||
OR ("state" = 'acknowledged'
|
||||
AND "delivery_digest" IS NOT NULL
|
||||
AND "acknowledged_at_ms" IS NOT NULL
|
||||
AND "complete_mutation_id" IS NULL
|
||||
AND "complete_request_id" IS NULL
|
||||
AND "revoked_credential_version" IS NULL
|
||||
AND "completed_at_ms" IS NULL
|
||||
AND "complete_audit_event_id" IS NULL)
|
||||
OR ("state" = 'completed'
|
||||
AND "delivery_digest" IS NOT NULL
|
||||
AND "acknowledged_at_ms" IS NOT NULL
|
||||
AND "complete_mutation_id" IS NOT NULL
|
||||
AND "complete_request_id" IS NOT NULL
|
||||
AND "revoked_credential_version" IS NOT NULL
|
||||
AND "completed_at_ms" IS NOT NULL
|
||||
AND "complete_audit_event_id" IS NOT NULL)
|
||||
)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_local_owner_recovery_open_subject_uidx"
|
||||
ON "QingLong3LocalOwnerCredentialRecoveries" ("subject_id")
|
||||
WHERE "state" <> 'completed'
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_local_owner_recovery_replacement_uidx"
|
||||
ON "QingLong3LocalOwnerCredentialRecoveries" (
|
||||
"replacement_credential_id", "replacement_credential_version"
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_local_owner_recovery_complete_mutation_uidx"
|
||||
ON "QingLong3LocalOwnerCredentialRecoveries" ("complete_mutation_id")
|
||||
WHERE "complete_mutation_id" IS NOT NULL
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_local_owner_recovery_previous_idx"
|
||||
ON "QingLong3LocalOwnerCredentialRecoveries" (
|
||||
"previous_credential_id", "previous_credential_version", "state"
|
||||
)
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0022CapabilityV11Migration = defineLocalSqliteMigration({
|
||||
id: '0022-capability-v11',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 11,
|
||||
migration_id = '0021-local-owner-credential-recovery',
|
||||
capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 10
|
||||
AND migration_id = '0019-local-owner-pepper-catalog'
|
||||
AND capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0023LocalOwnerPepperMaterialGcMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0023-local-owner-pepper-material-gc',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3LocalOwnerPepperMaterialGc" (
|
||||
"prepare_mutation_id" TEXT PRIMARY KEY NOT NULL,
|
||||
"prepare_request_id" TEXT NOT NULL,
|
||||
"pepper_key_id" TEXT NOT NULL,
|
||||
"material_digest" TEXT NOT NULL,
|
||||
"backup_material_digest" TEXT NOT NULL,
|
||||
"active_pepper_key_id" TEXT NOT NULL,
|
||||
"active_generation" INTEGER NOT NULL,
|
||||
"active_material_digest" TEXT NOT NULL,
|
||||
"retention_policy_version" INTEGER NOT NULL,
|
||||
"acknowledgement_retention_ms" INTEGER NOT NULL,
|
||||
"audit_retention_ms" INTEGER NOT NULL,
|
||||
"backup_retention_ms" INTEGER NOT NULL,
|
||||
"retention_policy_digest" TEXT NOT NULL,
|
||||
"references_inspected_at_ms" INTEGER NOT NULL,
|
||||
"retention_eligible_at_ms" INTEGER NOT NULL,
|
||||
"prepared_at_ms" INTEGER NOT NULL,
|
||||
"prepare_audit_event_id" TEXT NOT NULL,
|
||||
"state" TEXT NOT NULL,
|
||||
"complete_mutation_id" TEXT,
|
||||
"complete_request_id" TEXT,
|
||||
"destruction_proof_digest" TEXT,
|
||||
"completed_at_ms" INTEGER,
|
||||
"complete_audit_event_id" TEXT,
|
||||
FOREIGN KEY ("pepper_key_id")
|
||||
REFERENCES "QingLong3LocalOwnerPepperKeys" ("pepper_key_id")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("active_pepper_key_id")
|
||||
REFERENCES "QingLong3LocalOwnerPepperKeys" ("pepper_key_id")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("active_generation")
|
||||
REFERENCES "QingLong3LocalOwnerPepperActivations" ("generation")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("prepare_audit_event_id")
|
||||
REFERENCES "QingLong3SecurityAuditEvents" ("event_id")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("complete_audit_event_id")
|
||||
REFERENCES "QingLong3SecurityAuditEvents" ("event_id")
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_local_owner_pepper_gc_mutation_check CHECK (
|
||||
length("prepare_mutation_id") = 36
|
||||
AND "prepare_audit_event_id" = "prepare_mutation_id"
|
||||
AND ("complete_mutation_id" IS NULL OR (
|
||||
length("complete_mutation_id") = 36
|
||||
AND "complete_mutation_id" <> "prepare_mutation_id"
|
||||
AND "complete_audit_event_id" = "complete_mutation_id"
|
||||
))
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_pepper_gc_request_check CHECK (
|
||||
length("prepare_request_id") BETWEEN 1 AND 128
|
||||
AND ("complete_request_id" IS NULL
|
||||
OR length("complete_request_id") BETWEEN 1 AND 128)
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_pepper_gc_key_check CHECK (
|
||||
length("pepper_key_id") BETWEEN 1 AND 64
|
||||
AND "pepper_key_id" NOT GLOB '*[^A-Za-z0-9._:-]*'
|
||||
AND length("active_pepper_key_id") BETWEEN 1 AND 64
|
||||
AND "active_pepper_key_id" NOT GLOB '*[^A-Za-z0-9._:-]*'
|
||||
AND "pepper_key_id" <> "active_pepper_key_id"
|
||||
AND "active_generation" BETWEEN 1 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_pepper_gc_digest_check CHECK (
|
||||
length("material_digest") = 64
|
||||
AND "material_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
AND length("backup_material_digest") = 64
|
||||
AND "backup_material_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
AND length("active_material_digest") = 64
|
||||
AND "active_material_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
AND length("retention_policy_digest") = 64
|
||||
AND "retention_policy_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
AND ("destruction_proof_digest" IS NULL OR (
|
||||
length("destruction_proof_digest") = 64
|
||||
AND "destruction_proof_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
))
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_pepper_gc_retention_check CHECK (
|
||||
"retention_policy_version" = 1
|
||||
AND "acknowledgement_retention_ms" BETWEEN 604800000 AND 315360000000
|
||||
AND "audit_retention_ms" BETWEEN 2592000000 AND 315360000000
|
||||
AND "backup_retention_ms" BETWEEN 2592000000 AND 315360000000
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_pepper_gc_time_check CHECK (
|
||||
"references_inspected_at_ms" = "prepared_at_ms"
|
||||
AND "retention_eligible_at_ms" <= "prepared_at_ms"
|
||||
AND "prepared_at_ms" >= 0
|
||||
AND ("completed_at_ms" IS NULL
|
||||
OR "completed_at_ms" >= "prepared_at_ms")
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_pepper_gc_shape_check CHECK (
|
||||
("state" = 'prepared'
|
||||
AND "complete_mutation_id" IS NULL
|
||||
AND "complete_request_id" IS NULL
|
||||
AND "destruction_proof_digest" IS NULL
|
||||
AND "completed_at_ms" IS NULL
|
||||
AND "complete_audit_event_id" IS NULL)
|
||||
OR ("state" = 'completed'
|
||||
AND "complete_mutation_id" IS NOT NULL
|
||||
AND "complete_request_id" IS NOT NULL
|
||||
AND "destruction_proof_digest" IS NOT NULL
|
||||
AND "completed_at_ms" IS NOT NULL
|
||||
AND "complete_audit_event_id" IS NOT NULL)
|
||||
)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_local_owner_pepper_gc_key_uidx"
|
||||
ON "QingLong3LocalOwnerPepperMaterialGc" ("pepper_key_id")
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_local_owner_pepper_gc_open_uidx"
|
||||
ON "QingLong3LocalOwnerPepperMaterialGc" ("state")
|
||||
WHERE "state" = 'prepared'
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_local_owner_pepper_gc_complete_mutation_uidx"
|
||||
ON "QingLong3LocalOwnerPepperMaterialGc" ("complete_mutation_id")
|
||||
WHERE "complete_mutation_id" IS NOT NULL
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_local_owner_pepper_gc_state_idx"
|
||||
ON "QingLong3LocalOwnerPepperMaterialGc" (
|
||||
"state", "retention_eligible_at_ms", "pepper_key_id"
|
||||
)
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0024CapabilityV12Migration = defineLocalSqliteMigration({
|
||||
id: '0024-capability-v12',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 12,
|
||||
migration_id = '0023-local-owner-pepper-material-gc',
|
||||
capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 11
|
||||
AND migration_id = '0021-local-owner-credential-recovery'
|
||||
AND capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0025LocalOwnerDeliveryAcknowledgementGcMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0025-local-owner-delivery-acknowledgement-gc',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3LocalOwnerDeliveryAcknowledgementGc" (
|
||||
"gc_mutation_id" TEXT PRIMARY KEY NOT NULL,
|
||||
"gc_request_id" TEXT NOT NULL,
|
||||
"acknowledgement_mutation_id" TEXT NOT NULL,
|
||||
"acknowledgement_kind" TEXT NOT NULL,
|
||||
"delivery_digest" TEXT NOT NULL,
|
||||
"acknowledged_at_ms" INTEGER NOT NULL,
|
||||
"acknowledgement_semantic_digest" TEXT NOT NULL,
|
||||
"bridge_clear_evidence_digest" TEXT NOT NULL,
|
||||
"retention_policy_version" INTEGER NOT NULL,
|
||||
"replay_retention_ms" INTEGER NOT NULL,
|
||||
"audit_retention_ms" INTEGER NOT NULL,
|
||||
"retention_policy_digest" TEXT NOT NULL,
|
||||
"retention_eligible_at_ms" INTEGER NOT NULL,
|
||||
"compacted_at_ms" INTEGER NOT NULL,
|
||||
"audit_event_id" TEXT NOT NULL,
|
||||
"provisioning_mutation_id" TEXT,
|
||||
"challenge_mutation_id" TEXT,
|
||||
FOREIGN KEY ("audit_event_id")
|
||||
REFERENCES "QingLong3SecurityAuditEvents" ("event_id")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("provisioning_mutation_id")
|
||||
REFERENCES "QingLong3LocalIdentityProvisionings" ("mutation_id")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("challenge_mutation_id")
|
||||
REFERENCES "QingLong3LocalOwnerBootstrapChallenges" ("issue_mutation_id")
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_local_owner_delivery_ack_gc_mutation_check CHECK (
|
||||
length("gc_mutation_id") = 36
|
||||
AND "audit_event_id" = "gc_mutation_id"
|
||||
AND length("acknowledgement_mutation_id") = 36
|
||||
AND "acknowledgement_mutation_id" <> "gc_mutation_id"
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_delivery_ack_gc_request_check CHECK (
|
||||
length("gc_request_id") BETWEEN 1 AND 128
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_delivery_ack_gc_digest_check CHECK (
|
||||
length("delivery_digest") = 64
|
||||
AND "delivery_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
AND length("acknowledgement_semantic_digest") = 64
|
||||
AND "acknowledgement_semantic_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
AND length("bridge_clear_evidence_digest") = 64
|
||||
AND "bridge_clear_evidence_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
AND length("retention_policy_digest") = 64
|
||||
AND "retention_policy_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_delivery_ack_gc_retention_check CHECK (
|
||||
"retention_policy_version" = 1
|
||||
AND "replay_retention_ms" BETWEEN 2592000000 AND 315360000000
|
||||
AND "audit_retention_ms" BETWEEN 2592000000 AND 315360000000
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_delivery_ack_gc_time_check CHECK (
|
||||
"acknowledged_at_ms" >= 0
|
||||
AND "retention_eligible_at_ms" <= "compacted_at_ms"
|
||||
AND "compacted_at_ms" >= "acknowledged_at_ms"
|
||||
),
|
||||
CONSTRAINT ql3_local_owner_delivery_ack_gc_shape_check CHECK (
|
||||
(
|
||||
"acknowledgement_kind" = 'credential'
|
||||
AND "provisioning_mutation_id" = "acknowledgement_mutation_id"
|
||||
AND "challenge_mutation_id" IS NULL
|
||||
)
|
||||
OR (
|
||||
"acknowledgement_kind" = 'challenge'
|
||||
AND "provisioning_mutation_id" IS NULL
|
||||
AND "challenge_mutation_id" = "acknowledgement_mutation_id"
|
||||
)
|
||||
)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_local_owner_delivery_ack_gc_ack_uidx"
|
||||
ON "QingLong3LocalOwnerDeliveryAcknowledgementGc" (
|
||||
"acknowledgement_mutation_id"
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_local_owner_delivery_ack_gc_compacted_idx"
|
||||
ON "QingLong3LocalOwnerDeliveryAcknowledgementGc" (
|
||||
"acknowledgement_kind", "compacted_at_ms", "acknowledgement_mutation_id"
|
||||
)
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0026CapabilityV13Migration = defineLocalSqliteMigration({
|
||||
id: '0026-capability-v13',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 13,
|
||||
migration_id = '0025-local-owner-delivery-acknowledgement-gc',
|
||||
capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 12
|
||||
AND migration_id = '0023-local-owner-pepper-material-gc'
|
||||
AND capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0027TaskDefinitionsMigration = defineLocalSqliteMigration({
|
||||
id: '0027-task-definitions',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3TaskDefinitions" (
|
||||
"project_id" TEXT NOT NULL,
|
||||
"task_id" TEXT NOT NULL,
|
||||
"current_revision" INTEGER NOT NULL,
|
||||
"created_at_ms" INTEGER NOT NULL,
|
||||
"updated_at_ms" INTEGER NOT NULL,
|
||||
PRIMARY KEY ("project_id", "task_id"),
|
||||
FOREIGN KEY ("project_id")
|
||||
REFERENCES "QingLong3Projects" ("id")
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_task_definitions_id_check CHECK (
|
||||
length("project_id") BETWEEN 1 AND 128
|
||||
AND length("task_id") BETWEEN 1 AND 128
|
||||
AND "project_id" NOT GLOB '*[' || char(0) || '-' || char(31) || char(127) || ']*'
|
||||
AND "task_id" NOT GLOB '*[' || char(0) || '-' || char(31) || char(127) || ']*'
|
||||
),
|
||||
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"
|
||||
)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE TABLE "QingLong3TaskDefinitionRevisions" (
|
||||
"project_id" TEXT NOT NULL,
|
||||
"task_id" TEXT NOT NULL,
|
||||
"revision" INTEGER NOT NULL,
|
||||
"mutation_id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"kind" TEXT NOT NULL,
|
||||
"spec_json" TEXT NOT NULL,
|
||||
"labels_json" TEXT NOT NULL,
|
||||
"enabled" INTEGER NOT NULL,
|
||||
"content_digest" TEXT NOT NULL,
|
||||
"created_at_ms" INTEGER NOT NULL,
|
||||
PRIMARY KEY ("project_id", "task_id", "revision"),
|
||||
FOREIGN KEY ("project_id", "task_id")
|
||||
REFERENCES "QingLong3TaskDefinitions" ("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_mutation_check CHECK (
|
||||
length("mutation_id") = 36
|
||||
AND substr("mutation_id", 9, 1) = '-'
|
||||
AND substr("mutation_id", 14, 1) = '-'
|
||||
AND substr("mutation_id", 19, 1) = '-'
|
||||
AND substr("mutation_id", 24, 1) = '-'
|
||||
AND replace("mutation_id", '-', '') NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_task_definition_revisions_name_check CHECK (
|
||||
length("name") BETWEEN 1 AND 255
|
||||
AND ("description" IS NULL OR 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 (
|
||||
json_valid("spec_json")
|
||||
AND json_type("spec_json") = 'object'
|
||||
AND length("spec_json") BETWEEN 1 AND 65536
|
||||
),
|
||||
CONSTRAINT ql3_task_definition_revisions_labels_check CHECK (
|
||||
json_valid("labels_json")
|
||||
AND json_type("labels_json") = 'object'
|
||||
AND length("labels_json") BETWEEN 2 AND 16384
|
||||
),
|
||||
CONSTRAINT ql3_task_definition_revisions_enabled_check CHECK (
|
||||
"enabled" IN (0, 1)
|
||||
),
|
||||
CONSTRAINT ql3_task_definition_revisions_digest_check CHECK (
|
||||
length("content_digest") = 64
|
||||
AND "content_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_task_definition_revisions_created_check CHECK (
|
||||
"created_at_ms" >= 0
|
||||
)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_task_definition_revisions_mutation_uidx"
|
||||
ON "QingLong3TaskDefinitionRevisions" ("mutation_id")
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_task_definition_revisions_project_kind_idx"
|
||||
ON "QingLong3TaskDefinitionRevisions" (
|
||||
"project_id", "kind", "enabled", "task_id", "revision"
|
||||
)
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0028CapabilityV14Migration = defineLocalSqliteMigration({
|
||||
id: '0028-capability-v14',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 14,
|
||||
migration_id = '0027-task-definitions',
|
||||
capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 13
|
||||
AND migration_id = '0025-local-owner-delivery-acknowledgement-gc'
|
||||
AND capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,261 @@
|
||||
import {
|
||||
createLocalTaskExecutionRevision,
|
||||
type LocalDispatchCommand,
|
||||
} from '@qinglong/runtime-core/local-dispatch';
|
||||
import {
|
||||
normalizeTaskDefinitionRecord,
|
||||
type TaskDefinitionRecord,
|
||||
} from '@qinglong/runtime-core/task-definition';
|
||||
import { compileLocalCommandTaskDefinition } from '@qinglong/runtime-core/task-definition-execution-compiler';
|
||||
import {
|
||||
BUILT_IN_COMMAND_TASK_SPEC_SCHEMA,
|
||||
createBuiltInTaskSpecSemanticRegistry,
|
||||
} from '@qinglong/runtime-core/task-spec-semantic';
|
||||
import { LocalSqliteDispatchDefinitionStore } from '../task-definition/dispatchDefinitionStore';
|
||||
import { defineLocalSqliteProgrammaticMigration } from './sqlMigration';
|
||||
|
||||
const REPLACEMENT_TABLE = `
|
||||
CREATE TABLE "QingLong3LocalTaskExecutionRevisions_v15" (
|
||||
"project_id" TEXT NOT NULL,
|
||||
"task_id" TEXT NOT NULL,
|
||||
"task_revision" TEXT NOT NULL,
|
||||
"executor_type" TEXT NOT NULL,
|
||||
"command_json" TEXT NOT NULL,
|
||||
"working_directory" TEXT,
|
||||
"timeout_ms" INTEGER,
|
||||
"context_ref" TEXT NOT NULL,
|
||||
"content_digest" TEXT NOT NULL,
|
||||
"created_at_ms" INTEGER NOT NULL,
|
||||
PRIMARY KEY ("project_id", "task_id", "task_revision"),
|
||||
CONSTRAINT ql3_local_revision_executor_check CHECK (
|
||||
"executor_type" = 'local_process'
|
||||
),
|
||||
CONSTRAINT ql3_local_revision_command_check CHECK (
|
||||
json_valid("command_json")
|
||||
AND json_type("command_json") = 'object'
|
||||
AND length("command_json") BETWEEN 1 AND 131072
|
||||
),
|
||||
CONSTRAINT ql3_local_revision_working_directory_check CHECK (
|
||||
"working_directory" IS NULL
|
||||
OR (length("working_directory") BETWEEN 1 AND 4096
|
||||
AND substr("working_directory", 1, 1) = '/')
|
||||
),
|
||||
CONSTRAINT ql3_local_revision_timeout_check CHECK (
|
||||
"timeout_ms" IS NULL OR "timeout_ms" BETWEEN 1 AND 31536000000
|
||||
),
|
||||
CONSTRAINT ql3_local_revision_digest_check CHECK (
|
||||
length("content_digest") = 64
|
||||
AND "content_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_local_revision_created_check CHECK ("created_at_ms" >= 0),
|
||||
FOREIGN KEY ("context_ref")
|
||||
REFERENCES "QingLong3LocalExecutionContextRecipes" ("context_ref")
|
||||
ON DELETE RESTRICT
|
||||
)
|
||||
`;
|
||||
|
||||
type RevisionRow = Record<string, unknown>;
|
||||
|
||||
function requiredText(row: RevisionRow, key: string): string {
|
||||
const value = row[key];
|
||||
if (typeof value !== 'string') {
|
||||
throw new TypeError(`Local execution revision ${key} is invalid`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function optionalText(row: RevisionRow, key: string): string | undefined {
|
||||
const value = row[key];
|
||||
if (value === null) return undefined;
|
||||
return requiredText(row, key);
|
||||
}
|
||||
|
||||
function requiredInteger(row: RevisionRow, key: string): number {
|
||||
const value = row[key];
|
||||
if (!Number.isSafeInteger(value)) {
|
||||
throw new TypeError(`Local execution revision ${key} is invalid`);
|
||||
}
|
||||
return value as number;
|
||||
}
|
||||
|
||||
function optionalInteger(row: RevisionRow, key: string): number | undefined {
|
||||
const value = row[key];
|
||||
if (value === null) return undefined;
|
||||
return requiredInteger(row, key);
|
||||
}
|
||||
|
||||
function command(row: RevisionRow): LocalDispatchCommand {
|
||||
try {
|
||||
return JSON.parse(requiredText(row, 'commandJson')) as LocalDispatchCommand;
|
||||
} catch {
|
||||
throw new TypeError('Local execution revision commandJson is invalid');
|
||||
}
|
||||
}
|
||||
|
||||
function requiredJson(row: RevisionRow, key: string): unknown {
|
||||
try {
|
||||
return JSON.parse(requiredText(row, key)) as unknown;
|
||||
} catch {
|
||||
throw new TypeError(`TaskDefinition ${key} is invalid`);
|
||||
}
|
||||
}
|
||||
|
||||
function taskDefinition(row: RevisionRow): TaskDefinitionRecord {
|
||||
const description = optionalText(row, 'description');
|
||||
const enabled = requiredInteger(row, 'enabled');
|
||||
if (enabled !== 0 && enabled !== 1) {
|
||||
throw new TypeError('TaskDefinition enabled is invalid');
|
||||
}
|
||||
return normalizeTaskDefinitionRecord({
|
||||
projectId: requiredText(row, 'projectId'),
|
||||
taskId: requiredText(row, 'taskId'),
|
||||
revision: requiredInteger(row, 'revision'),
|
||||
mutationId: requiredText(row, 'mutationId'),
|
||||
name: requiredText(row, 'name'),
|
||||
...(description === undefined ? {} : { description }),
|
||||
kind: requiredText(row, 'kind') as TaskDefinitionRecord['kind'],
|
||||
spec: requiredJson(row, 'specJson') as TaskDefinitionRecord['spec'],
|
||||
labels: requiredJson(row, 'labelsJson') as TaskDefinitionRecord['labels'],
|
||||
enabled: enabled === 1,
|
||||
contentDigest: requiredText(row, 'taskContentDigest'),
|
||||
createdAtMs: requiredInteger(row, 'taskCreatedAtMs'),
|
||||
updatedAtMs: requiredInteger(row, 'taskUpdatedAtMs'),
|
||||
});
|
||||
}
|
||||
|
||||
export const local0029LocalExecutionRevisionDigestMigration =
|
||||
defineLocalSqliteProgrammaticMigration({
|
||||
id: '0029-local-execution-revision-digest',
|
||||
program: [
|
||||
REPLACEMENT_TABLE,
|
||||
`Read every legacy revision in primary-key order without buffering.
|
||||
Parse and normalize command_json through
|
||||
createLocalTaskExecutionRevision using digest domain
|
||||
qinglong.local-task-execution-revision.v1 NUL. The digest covers
|
||||
project, task, revision, executor, canonical command, optional working
|
||||
directory, optional timeout and context ref; created_at_ms is excluded
|
||||
so an exact semantic replay preserves the first observed timestamp.`,
|
||||
`Insert every normalized row and computed lowercase SHA-256 digest into
|
||||
QingLong3LocalTaskExecutionRevisions_v15. Reject malformed legacy rows,
|
||||
count drift, constraint failures or digest failures so the enclosing
|
||||
BEGIN IMMEDIATE transaction rolls the entire migration back.`,
|
||||
`After the digest table replacement, read every historical enabled
|
||||
qinglong/command@v1 TaskDefinition revision in primary-key order,
|
||||
revalidate its stored TaskDefinition content digest and built-in
|
||||
semantics, compile the deterministic local plan, and append its context
|
||||
recipe and execution revision through exact-content replay. Skip
|
||||
disabled and non-built-in revisions; reject missing Projects, corrupt
|
||||
built-in records or pre-existing derived identity conflicts.`,
|
||||
'DROP TABLE "QingLong3LocalTaskExecutionRevisions"',
|
||||
`ALTER TABLE "QingLong3LocalTaskExecutionRevisions_v15"
|
||||
RENAME TO "QingLong3LocalTaskExecutionRevisions"`,
|
||||
],
|
||||
up({ client }) {
|
||||
client.exec(REPLACEMENT_TABLE);
|
||||
const rows = client.prepare(
|
||||
`SELECT "project_id" AS "projectId", "task_id" AS "taskId",
|
||||
"task_revision" AS "taskRevision",
|
||||
"executor_type" AS "executorType",
|
||||
"command_json" AS "commandJson",
|
||||
"working_directory" AS "workingDirectory",
|
||||
"timeout_ms" AS "timeoutMs", "context_ref" AS "contextRef",
|
||||
"created_at_ms" AS "createdAtMs"
|
||||
FROM "QingLong3LocalTaskExecutionRevisions"
|
||||
ORDER BY "project_id", "task_id", "task_revision"`,
|
||||
);
|
||||
const insert = client.prepare(
|
||||
`INSERT INTO "QingLong3LocalTaskExecutionRevisions_v15" (
|
||||
"project_id", "task_id", "task_revision", "executor_type",
|
||||
"command_json", "working_directory", "timeout_ms", "context_ref",
|
||||
"content_digest", "created_at_ms"
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
);
|
||||
let backfilled = 0;
|
||||
for (const value of rows.iterate() as Iterable<RevisionRow>) {
|
||||
const executorType = requiredText(value, 'executorType');
|
||||
if (executorType !== 'local_process') {
|
||||
throw new TypeError('Local execution revision executorType is invalid');
|
||||
}
|
||||
const workingDirectory = optionalText(value, 'workingDirectory');
|
||||
const timeoutMs = optionalInteger(value, 'timeoutMs');
|
||||
const revision = createLocalTaskExecutionRevision({
|
||||
projectId: requiredText(value, 'projectId'),
|
||||
taskId: requiredText(value, 'taskId'),
|
||||
taskRevision: requiredText(value, 'taskRevision'),
|
||||
executorType,
|
||||
command: command(value),
|
||||
...(workingDirectory === undefined ? {} : { workingDirectory }),
|
||||
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
||||
contextRef: requiredText(value, 'contextRef'),
|
||||
createdAtMs: requiredInteger(value, 'createdAtMs'),
|
||||
});
|
||||
insert.run(
|
||||
revision.projectId,
|
||||
revision.taskId,
|
||||
revision.taskRevision,
|
||||
revision.executorType,
|
||||
JSON.stringify(revision.command),
|
||||
revision.workingDirectory ?? null,
|
||||
revision.timeoutMs ?? null,
|
||||
revision.contextRef,
|
||||
revision.contentDigest,
|
||||
revision.createdAtMs,
|
||||
);
|
||||
backfilled += 1;
|
||||
}
|
||||
const sourceCount = client
|
||||
.prepare(
|
||||
'SELECT COUNT(*) AS "count" FROM "QingLong3LocalTaskExecutionRevisions"',
|
||||
)
|
||||
.get() as { count?: unknown } | undefined;
|
||||
if (
|
||||
!sourceCount ||
|
||||
!Number.isSafeInteger(sourceCount.count) ||
|
||||
sourceCount.count !== backfilled
|
||||
) {
|
||||
throw new TypeError('Local execution revision backfill count drifted');
|
||||
}
|
||||
client.exec('DROP TABLE "QingLong3LocalTaskExecutionRevisions"');
|
||||
client.exec(
|
||||
`ALTER TABLE "QingLong3LocalTaskExecutionRevisions_v15"
|
||||
RENAME TO "QingLong3LocalTaskExecutionRevisions"`,
|
||||
);
|
||||
|
||||
const semanticRegistry = createBuiltInTaskSpecSemanticRegistry();
|
||||
const definitions = client.prepare(
|
||||
`SELECT revision."project_id" AS "projectId",
|
||||
revision."task_id" AS "taskId",
|
||||
revision."revision" AS "revision",
|
||||
revision."mutation_id" AS "mutationId",
|
||||
revision."name" AS "name",
|
||||
revision."description" AS "description",
|
||||
revision."kind" AS "kind",
|
||||
revision."spec_json" AS "specJson",
|
||||
revision."labels_json" AS "labelsJson",
|
||||
revision."enabled" AS "enabled",
|
||||
revision."content_digest" AS "taskContentDigest",
|
||||
head."created_at_ms" AS "taskCreatedAtMs",
|
||||
revision."created_at_ms" AS "taskUpdatedAtMs"
|
||||
FROM "QingLong3TaskDefinitionRevisions" AS revision
|
||||
JOIN "QingLong3TaskDefinitions" AS head
|
||||
ON head."project_id" = revision."project_id"
|
||||
AND head."task_id" = revision."task_id"
|
||||
ORDER BY revision."project_id", revision."task_id",
|
||||
revision."revision"`,
|
||||
);
|
||||
const dispatchDefinitions = new LocalSqliteDispatchDefinitionStore(client);
|
||||
for (const value of definitions.iterate() as Iterable<RevisionRow>) {
|
||||
const definition = taskDefinition(value);
|
||||
if (
|
||||
!definition.enabled ||
|
||||
definition.kind !== 'command' ||
|
||||
definition.spec.schema !== BUILT_IN_COMMAND_TASK_SPEC_SCHEMA
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
dispatchDefinitions.appendPlan(
|
||||
compileLocalCommandTaskDefinition(definition, semanticRegistry),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0030CapabilityV15Migration = defineLocalSqliteMigration({
|
||||
id: '0030-capability-v15',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 15,
|
||||
migration_id = '0029-local-execution-revision-digest',
|
||||
capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 14
|
||||
AND migration_id = '0027-task-definitions'
|
||||
AND capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0031TriggerDefinitionsMigration = defineLocalSqliteMigration({
|
||||
id: '0031-trigger-definitions',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3Triggers" (
|
||||
"project_id" TEXT NOT NULL,
|
||||
"trigger_id" TEXT NOT NULL,
|
||||
"task_id" TEXT NOT NULL,
|
||||
"current_revision" INTEGER NOT NULL,
|
||||
"created_at_ms" INTEGER NOT NULL,
|
||||
"updated_at_ms" INTEGER NOT NULL,
|
||||
PRIMARY KEY ("project_id", "trigger_id"),
|
||||
FOREIGN KEY ("project_id")
|
||||
REFERENCES "QingLong3Projects" ("id")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("project_id", "task_id")
|
||||
REFERENCES "QingLong3TaskDefinitions" ("project_id", "task_id")
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_triggers_id_check CHECK (
|
||||
length("project_id") BETWEEN 1 AND 128
|
||||
AND length("trigger_id") BETWEEN 1 AND 128
|
||||
AND length("task_id") BETWEEN 1 AND 128
|
||||
AND "project_id" NOT GLOB '*[' || char(0) || '-' || char(31) || char(127) || ']*'
|
||||
AND "trigger_id" NOT GLOB '*[' || char(0) || '-' || char(31) || char(127) || ']*'
|
||||
AND "task_id" NOT GLOB '*[' || char(0) || '-' || char(31) || char(127) || ']*'
|
||||
),
|
||||
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"
|
||||
)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_triggers_task_uidx"
|
||||
ON "QingLong3Triggers" ("project_id", "trigger_id", "task_id")
|
||||
`,
|
||||
`
|
||||
CREATE TABLE "QingLong3TriggerRevisions" (
|
||||
"project_id" TEXT NOT NULL,
|
||||
"trigger_id" TEXT NOT NULL,
|
||||
"revision" INTEGER NOT NULL,
|
||||
"mutation_id" TEXT NOT NULL,
|
||||
"task_id" TEXT NOT NULL,
|
||||
"task_revision" INTEGER NOT NULL,
|
||||
"task_content_digest" TEXT NOT NULL,
|
||||
"spec_json" TEXT NOT NULL,
|
||||
"enabled" INTEGER NOT NULL,
|
||||
"content_digest" TEXT NOT NULL,
|
||||
"created_at_ms" INTEGER NOT NULL,
|
||||
PRIMARY KEY ("project_id", "trigger_id", "revision"),
|
||||
FOREIGN KEY ("project_id", "trigger_id", "task_id")
|
||||
REFERENCES "QingLong3Triggers" ("project_id", "trigger_id", "task_id")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("project_id", "task_id", "task_revision")
|
||||
REFERENCES "QingLong3TaskDefinitionRevisions" ("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_mutation_check CHECK (
|
||||
length("mutation_id") = 36
|
||||
AND substr("mutation_id", 9, 1) = '-'
|
||||
AND substr("mutation_id", 14, 1) = '-'
|
||||
AND substr("mutation_id", 19, 1) = '-'
|
||||
AND substr("mutation_id", 24, 1) = '-'
|
||||
AND replace("mutation_id", '-', '') NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_trigger_revisions_task_digest_check CHECK (
|
||||
length("task_content_digest") = 64
|
||||
AND "task_content_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_trigger_revisions_spec_check CHECK (
|
||||
json_valid("spec_json")
|
||||
AND json_type("spec_json") = 'object'
|
||||
AND length("spec_json") BETWEEN 1 AND 16384
|
||||
),
|
||||
CONSTRAINT ql3_trigger_revisions_enabled_check CHECK (
|
||||
"enabled" IN (0, 1)
|
||||
),
|
||||
CONSTRAINT ql3_trigger_revisions_digest_check CHECK (
|
||||
length("content_digest") = 64
|
||||
AND "content_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_trigger_revisions_created_check CHECK (
|
||||
"created_at_ms" >= 0
|
||||
)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_trigger_revisions_mutation_uidx"
|
||||
ON "QingLong3TriggerRevisions" ("mutation_id")
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_trigger_revisions_project_enabled_idx"
|
||||
ON "QingLong3TriggerRevisions" (
|
||||
"project_id", "enabled", "trigger_id", "revision"
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_trigger_revisions_task_idx"
|
||||
ON "QingLong3TriggerRevisions" (
|
||||
"project_id", "task_id", "task_revision", "trigger_id", "revision"
|
||||
)
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0032CapabilityV16Migration = defineLocalSqliteMigration({
|
||||
id: '0032-capability-v16',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 16,
|
||||
migration_id = '0031-trigger-definitions',
|
||||
capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 15
|
||||
AND migration_id = '0029-local-execution-revision-digest'
|
||||
AND capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0033LegacyAdoptionLedgerMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0033-legacy-adoption-ledger',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3LegacyAdoptions" (
|
||||
"mutation_id" TEXT PRIMARY KEY,
|
||||
"decision_id" TEXT NOT NULL,
|
||||
"project_id" TEXT NOT NULL,
|
||||
"profile" TEXT NOT NULL,
|
||||
"plan_digest" TEXT NOT NULL,
|
||||
"inventory_digest" TEXT NOT NULL,
|
||||
"decision_digest" TEXT NOT NULL,
|
||||
"receipt_digest" TEXT NOT NULL,
|
||||
"authorization_file_digest" TEXT NOT NULL,
|
||||
"publication_digest" TEXT NOT NULL,
|
||||
"row_count" INTEGER NOT NULL,
|
||||
"adopted_task_count" INTEGER NOT NULL,
|
||||
"adopted_trigger_count" INTEGER NOT NULL,
|
||||
"skipped_count" INTEGER NOT NULL,
|
||||
"audit_event_id" TEXT NOT NULL,
|
||||
"created_at_ms" INTEGER NOT NULL,
|
||||
FOREIGN KEY ("project_id")
|
||||
REFERENCES "QingLong3Projects" ("id")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("audit_event_id")
|
||||
REFERENCES "QingLong3SecurityAuditEvents" ("event_id")
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_legacy_adoptions_mutation_check CHECK (
|
||||
length("mutation_id") = 36
|
||||
AND "audit_event_id" = "mutation_id"
|
||||
),
|
||||
CONSTRAINT ql3_legacy_adoptions_decision_check CHECK (
|
||||
length("decision_id") = 36
|
||||
AND substr("decision_id", 15, 1) = '7'
|
||||
AND replace("decision_id", '-', '') NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_legacy_adoptions_profile_check CHECK (
|
||||
"profile" IN ('edge', 'standalone')
|
||||
),
|
||||
CONSTRAINT ql3_legacy_adoptions_digest_check CHECK (
|
||||
length("plan_digest") = 64
|
||||
AND "plan_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
AND length("inventory_digest") = 64
|
||||
AND "inventory_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
AND length("decision_digest") = 64
|
||||
AND "decision_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
AND length("receipt_digest") = 64
|
||||
AND "receipt_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
AND length("authorization_file_digest") = 64
|
||||
AND "authorization_file_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
AND length("publication_digest") = 64
|
||||
AND "publication_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_legacy_adoptions_count_check CHECK (
|
||||
"row_count" BETWEEN 0 AND 100000
|
||||
AND "adopted_task_count" BETWEEN 0 AND "row_count"
|
||||
AND "skipped_count" BETWEEN 0 AND "row_count"
|
||||
AND "adopted_task_count" + "skipped_count" = "row_count"
|
||||
AND "adopted_trigger_count" BETWEEN 0 AND 500000
|
||||
),
|
||||
CONSTRAINT ql3_legacy_adoptions_created_check CHECK (
|
||||
"created_at_ms" >= 0
|
||||
)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_legacy_adoptions_decision_uidx"
|
||||
ON "QingLong3LegacyAdoptions" ("decision_id")
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_legacy_adoptions_project_time_idx"
|
||||
ON "QingLong3LegacyAdoptions" (
|
||||
"project_id", "created_at_ms" DESC, "mutation_id" DESC
|
||||
)
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0034CapabilityV17Migration = defineLocalSqliteMigration({
|
||||
id: '0034-capability-v17',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 17,
|
||||
migration_id = '0033-legacy-adoption-ledger',
|
||||
capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 16
|
||||
AND migration_id = '0031-trigger-definitions'
|
||||
AND capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0035LocalSchedulerMigration = defineLocalSqliteMigration({
|
||||
id: '0035-local-scheduler',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3LocalTriggerSchedules" (
|
||||
project_id TEXT NOT NULL,
|
||||
trigger_id TEXT NOT NULL,
|
||||
trigger_revision INTEGER NOT NULL,
|
||||
next_fire_at_ms INTEGER,
|
||||
last_scheduled_at_ms INTEGER,
|
||||
state_version INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
PRIMARY KEY (project_id, trigger_id),
|
||||
CONSTRAINT ql3_local_trigger_schedules_trigger_fk
|
||||
FOREIGN KEY (project_id, trigger_id)
|
||||
REFERENCES "QingLong3Triggers" (project_id, trigger_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_local_trigger_schedules_revision_check
|
||||
CHECK (trigger_revision BETWEEN 1 AND 2147483647),
|
||||
CONSTRAINT ql3_local_trigger_schedules_state_check
|
||||
CHECK (state_version >= 0),
|
||||
CONSTRAINT ql3_local_trigger_schedules_time_check CHECK (
|
||||
updated_at_ms >= 0 AND
|
||||
(next_fire_at_ms IS NULL OR next_fire_at_ms >= 0) AND
|
||||
(last_scheduled_at_ms IS NULL OR last_scheduled_at_ms >= 0)
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE INDEX ql3_local_trigger_schedules_due_idx ON "QingLong3LocalTriggerSchedules" (next_fire_at_ms, project_id, trigger_id) WHERE next_fire_at_ms IS NOT NULL`,
|
||||
`CREATE INDEX ql3_local_trigger_schedules_initialize_idx ON "QingLong3LocalTriggerSchedules" (project_id, trigger_id) WHERE next_fire_at_ms IS NULL`,
|
||||
`
|
||||
INSERT INTO "QingLong3LocalTriggerSchedules" (
|
||||
project_id, trigger_id, trigger_revision, next_fire_at_ms,
|
||||
last_scheduled_at_ms, state_version, updated_at_ms
|
||||
)
|
||||
SELECT project_id, trigger_id, current_revision, NULL, NULL, 0, updated_at_ms
|
||||
FROM "QingLong3Triggers"
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0036CapabilityV18Migration = defineLocalSqliteMigration({
|
||||
id: '0036-capability-v18',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 18,
|
||||
migration_id = '0035-local-scheduler',
|
||||
capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 17
|
||||
AND migration_id = '0033-legacy-adoption-ledger'
|
||||
AND capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0037PluginPackageInstallsMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0037-plugin-package-installs',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3PluginPackageInstalls" (
|
||||
installation_id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL,
|
||||
package_name TEXT NOT NULL,
|
||||
package_version TEXT NOT NULL,
|
||||
operation TEXT NOT NULL,
|
||||
lock_digest TEXT NOT NULL,
|
||||
target_generation INTEGER NOT NULL,
|
||||
previous_active_lock_digest TEXT,
|
||||
active_lock_digest TEXT,
|
||||
state TEXT NOT NULL,
|
||||
version INTEGER NOT NULL,
|
||||
last_mutation_id TEXT NOT NULL,
|
||||
last_mutation_digest TEXT NOT NULL,
|
||||
lock_json TEXT NOT NULL,
|
||||
record_json TEXT NOT NULL,
|
||||
record_digest TEXT NOT NULL,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_installs_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "QingLong3Projects" (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_installs_identity_check CHECK (
|
||||
length(installation_id) BETWEEN 1 AND 128 AND
|
||||
length(package_name) BETWEEN 1 AND 253 AND
|
||||
length(package_version) BETWEEN 1 AND 128 AND
|
||||
length(last_mutation_id) BETWEEN 1 AND 128
|
||||
),
|
||||
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 (
|
||||
length(lock_digest) = 64 AND lock_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
(previous_active_lock_digest IS NULL OR
|
||||
(length(previous_active_lock_digest) = 64 AND
|
||||
previous_active_lock_digest NOT GLOB '*[^0-9a-f]*')) AND
|
||||
(active_lock_digest IS NULL OR
|
||||
(length(active_lock_digest) = 64 AND
|
||||
active_lock_digest NOT GLOB '*[^0-9a-f]*')) AND
|
||||
length(last_mutation_digest) = 64 AND
|
||||
last_mutation_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(record_digest) = 64 AND record_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_installs_record_check CHECK (
|
||||
length(lock_json) BETWEEN 2 AND 262144 AND
|
||||
json_valid(lock_json) AND json_type(lock_json) = 'object' AND
|
||||
json_extract(lock_json, '$.lockDigest') = lock_digest AND
|
||||
json_extract(lock_json, '$.projectId') = project_id AND
|
||||
json_extract(lock_json, '$.packageName') = package_name AND
|
||||
length(record_json) BETWEEN 2 AND 262144 AND
|
||||
json_valid(record_json) AND json_type(record_json) = 'object' AND
|
||||
json_extract(record_json, '$.installationId') = installation_id AND
|
||||
json_extract(record_json, '$.projectId') = project_id AND
|
||||
json_extract(record_json, '$.packageName') = package_name AND
|
||||
json_extract(record_json, '$.lockDigest') = lock_digest AND
|
||||
json_extract(record_json, '$.state') = state AND
|
||||
json_extract(record_json, '$.version') = version AND
|
||||
json_extract(record_json, '$.recordDigest') = record_digest
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_installs_time_check CHECK (
|
||||
created_at_ms >= 0 AND updated_at_ms >= created_at_ms
|
||||
)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE TABLE "QingLong3PluginPackageInstallHeads" (
|
||||
project_id TEXT NOT NULL,
|
||||
package_name TEXT NOT NULL,
|
||||
installation_id TEXT NOT NULL,
|
||||
PRIMARY KEY (project_id, package_name),
|
||||
CONSTRAINT ql3_plugin_package_install_heads_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "QingLong3Projects" (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_install_heads_install_fk
|
||||
FOREIGN KEY (installation_id)
|
||||
REFERENCES "QingLong3PluginPackageInstalls" (installation_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_install_heads_identity_check CHECK (
|
||||
length(package_name) BETWEEN 1 AND 253 AND
|
||||
length(installation_id) BETWEEN 1 AND 128
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_install_heads_install_uidx ON "QingLong3PluginPackageInstallHeads" (installation_id)`,
|
||||
`
|
||||
CREATE TABLE "QingLong3PluginPackageInstallMutations" (
|
||||
installation_id TEXT NOT NULL,
|
||||
mutation_id TEXT NOT NULL,
|
||||
mutation_digest TEXT NOT NULL,
|
||||
resulting_record_digest TEXT NOT NULL,
|
||||
occurred_at_ms INTEGER NOT NULL,
|
||||
PRIMARY KEY (installation_id, mutation_id),
|
||||
CONSTRAINT ql3_plugin_package_install_mutations_install_fk
|
||||
FOREIGN KEY (installation_id)
|
||||
REFERENCES "QingLong3PluginPackageInstalls" (installation_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_install_mutations_identity_check
|
||||
CHECK (length(mutation_id) BETWEEN 1 AND 128),
|
||||
CONSTRAINT ql3_plugin_package_install_mutations_digest_check CHECK (
|
||||
length(mutation_digest) = 64 AND
|
||||
mutation_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(resulting_record_digest) = 64 AND
|
||||
resulting_record_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_install_mutations_time_check
|
||||
CHECK (occurred_at_ms >= 0)
|
||||
)
|
||||
`,
|
||||
`CREATE INDEX ql3_plugin_package_installs_recovery_idx ON "QingLong3PluginPackageInstalls" (state, package_name, installation_id) WHERE state IN ('queued','staged','activating')`,
|
||||
`CREATE INDEX ql3_plugin_package_installs_project_history_idx ON "QingLong3PluginPackageInstalls" (project_id, package_name, created_at_ms, installation_id)`,
|
||||
`CREATE INDEX ql3_plugin_package_install_mutations_result_idx ON "QingLong3PluginPackageInstallMutations" (installation_id, resulting_record_digest)`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0038CapabilityV19Migration = defineLocalSqliteMigration({
|
||||
id: '0038-capability-v19',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 19,
|
||||
migration_id = '0037-plugin-package-installs',
|
||||
capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 18
|
||||
AND migration_id = '0035-local-scheduler'
|
||||
AND capabilities = '{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0039ApprovedActionsMigration = defineLocalSqliteMigration({
|
||||
id: '0039-approved-actions',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3ApprovalRequests" (
|
||||
request_id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL,
|
||||
version INTEGER NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
action_type TEXT NOT NULL,
|
||||
action_ref TEXT NOT NULL,
|
||||
action_digest TEXT NOT NULL,
|
||||
preview_digest TEXT NOT NULL,
|
||||
requested_by_type TEXT NOT NULL,
|
||||
requested_by_id TEXT NOT NULL,
|
||||
decision_id TEXT,
|
||||
consumption_id TEXT,
|
||||
dispatch_id TEXT,
|
||||
expires_at_ms INTEGER NOT NULL,
|
||||
request_json TEXT NOT NULL,
|
||||
request_digest TEXT NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
CONSTRAINT ql3_approval_requests_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "QingLong3Projects" (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_approval_requests_identity_check CHECK (
|
||||
length(request_id) BETWEEN 1 AND 128 AND
|
||||
length(action_type) BETWEEN 1 AND 128 AND
|
||||
length(action_ref) BETWEEN 1 AND 255
|
||||
),
|
||||
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 (
|
||||
length(action_digest) = 64 AND action_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(preview_digest) = 64 AND preview_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(request_digest) = 64 AND request_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_approval_requests_subject_check CHECK (
|
||||
requested_by_type IN
|
||||
('user','api_app','mcp_client','agent','system','worker') AND
|
||||
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 (
|
||||
length(request_json) BETWEEN 2 AND 65536 AND
|
||||
json_valid(request_json) AND json_type(request_json) = 'object' AND
|
||||
json_extract(request_json, '$.id') = request_id AND
|
||||
json_extract(request_json, '$.projectId') = project_id AND
|
||||
json_extract(request_json, '$.version') = version AND
|
||||
json_extract(request_json, '$.state') = state AND
|
||||
json_extract(request_json, '$.action.actionType') = action_type AND
|
||||
json_extract(request_json, '$.action.actionRef') = action_ref AND
|
||||
json_extract(request_json, '$.action.actionDigest') = action_digest AND
|
||||
json_extract(request_json, '$.action.previewDigest') = preview_digest
|
||||
),
|
||||
CONSTRAINT ql3_approval_requests_time_check
|
||||
CHECK (expires_at_ms > 0 AND updated_at_ms >= 0)
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_approval_requests_decision_uidx ON "QingLong3ApprovalRequests" (decision_id)`,
|
||||
`CREATE UNIQUE INDEX ql3_approval_requests_consumption_uidx ON "QingLong3ApprovalRequests" (consumption_id)`,
|
||||
`CREATE UNIQUE INDEX ql3_approval_requests_dispatch_uidx ON "QingLong3ApprovalRequests" (dispatch_id)`,
|
||||
`CREATE INDEX ql3_approval_requests_pending_idx ON "QingLong3ApprovalRequests" (expires_at_ms, request_id) WHERE state = 'pending'`,
|
||||
`CREATE INDEX ql3_approval_requests_project_idx ON "QingLong3ApprovalRequests" (project_id, updated_at_ms, request_id)`,
|
||||
`
|
||||
CREATE TABLE "QingLong3ApprovedActionDispatches" (
|
||||
dispatch_id TEXT PRIMARY KEY,
|
||||
approval_request_id TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
action_type TEXT NOT NULL,
|
||||
action_ref TEXT NOT NULL,
|
||||
action_digest TEXT NOT NULL,
|
||||
preview_digest TEXT NOT NULL,
|
||||
dispatch_json TEXT NOT NULL,
|
||||
dispatch_digest TEXT NOT NULL,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
CONSTRAINT ql3_approved_action_dispatch_request_fk
|
||||
FOREIGN KEY (approval_request_id)
|
||||
REFERENCES "QingLong3ApprovalRequests" (request_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_approved_action_dispatch_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "QingLong3Projects" (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_approved_action_dispatch_identity_check CHECK (
|
||||
length(dispatch_id) BETWEEN 1 AND 128 AND
|
||||
length(action_type) BETWEEN 1 AND 128 AND
|
||||
length(action_ref) BETWEEN 1 AND 255
|
||||
),
|
||||
CONSTRAINT ql3_approved_action_dispatch_digest_check CHECK (
|
||||
length(action_digest) = 64 AND action_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(preview_digest) = 64 AND preview_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(dispatch_digest) = 64 AND dispatch_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_approved_action_dispatch_json_check CHECK (
|
||||
length(dispatch_json) BETWEEN 2 AND 65536 AND
|
||||
json_valid(dispatch_json) AND json_type(dispatch_json) = 'object' AND
|
||||
json_extract(dispatch_json, '$.id') = dispatch_id AND
|
||||
json_extract(dispatch_json, '$.approvalRequestId') = approval_request_id AND
|
||||
json_extract(dispatch_json, '$.projectId') = project_id AND
|
||||
json_extract(dispatch_json, '$.action.actionType') = action_type AND
|
||||
json_extract(dispatch_json, '$.action.actionRef') = action_ref AND
|
||||
json_extract(dispatch_json, '$.action.actionDigest') = action_digest AND
|
||||
json_extract(dispatch_json, '$.action.previewDigest') = preview_digest
|
||||
),
|
||||
CONSTRAINT ql3_approved_action_dispatch_time_check
|
||||
CHECK (created_at_ms >= 0)
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_approved_action_dispatch_request_uidx ON "QingLong3ApprovedActionDispatches" (approval_request_id)`,
|
||||
`CREATE INDEX ql3_approved_action_dispatch_project_idx ON "QingLong3ApprovedActionDispatches" (project_id, created_at_ms, dispatch_id)`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
const PREVIOUS_CAPABILITIES =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1}';
|
||||
const NEXT_CAPABILITIES =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1}';
|
||||
|
||||
export const local0040CapabilityV20Migration = defineLocalSqliteMigration({
|
||||
id: '0040-capability-v20',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 20,
|
||||
migration_id = '0039-approved-actions',
|
||||
capabilities = '${NEXT_CAPABILITIES}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 19
|
||||
AND migration_id = '0037-plugin-package-installs'
|
||||
AND capabilities = '${PREVIOUS_CAPABILITIES}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0041PluginPackageAdmissionReceiptsMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0041-plugin-package-admission-receipts',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3PluginPackageAdmissionReceipts" (
|
||||
dispatch_id TEXT PRIMARY KEY,
|
||||
dispatch_digest TEXT NOT NULL,
|
||||
approval_request_id TEXT NOT NULL,
|
||||
action_ref TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
package_name TEXT NOT NULL,
|
||||
installation_id TEXT NOT NULL,
|
||||
lock_digest TEXT NOT NULL,
|
||||
record_digest TEXT NOT NULL,
|
||||
mutation_id TEXT NOT NULL,
|
||||
mutation_digest TEXT NOT NULL,
|
||||
audit_event_id TEXT NOT NULL,
|
||||
admitted_at_ms INTEGER NOT NULL,
|
||||
receipt_json TEXT NOT NULL,
|
||||
receipt_digest TEXT NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_admission_dispatch_fk
|
||||
FOREIGN KEY (dispatch_id)
|
||||
REFERENCES "QingLong3ApprovedActionDispatches" (dispatch_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_admission_request_fk
|
||||
FOREIGN KEY (approval_request_id)
|
||||
REFERENCES "QingLong3ApprovalRequests" (request_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_admission_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "QingLong3Projects" (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_admission_install_fk
|
||||
FOREIGN KEY (installation_id)
|
||||
REFERENCES "QingLong3PluginPackageInstalls" (installation_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_admission_audit_fk
|
||||
FOREIGN KEY (audit_event_id)
|
||||
REFERENCES "QingLong3SecurityAuditEvents" (event_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_admission_identity_check CHECK (
|
||||
length(dispatch_id) BETWEEN 1 AND 128 AND
|
||||
length(approval_request_id) BETWEEN 1 AND 128 AND
|
||||
length(action_ref) BETWEEN 1 AND 255 AND
|
||||
length(project_id) BETWEEN 1 AND 128 AND
|
||||
length(package_name) BETWEEN 1 AND 64 AND
|
||||
length(installation_id) BETWEEN 1 AND 128 AND
|
||||
length(mutation_id) BETWEEN 1 AND 128 AND
|
||||
length(audit_event_id) = 36
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_admission_digest_check CHECK (
|
||||
length(dispatch_digest) = 64 AND
|
||||
dispatch_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(lock_digest) = 64 AND lock_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(record_digest) = 64 AND record_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(mutation_digest) = 64 AND
|
||||
mutation_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(receipt_digest) = 64 AND receipt_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_admission_json_check CHECK (
|
||||
length(receipt_json) BETWEEN 2 AND 65536 AND
|
||||
json_valid(receipt_json) AND json_type(receipt_json) = 'object' AND
|
||||
json_extract(receipt_json, '$.schema') =
|
||||
'qinglong/plugin-package-admission-receipt@v1' AND
|
||||
json_extract(receipt_json, '$.dispatchId') = dispatch_id AND
|
||||
json_extract(receipt_json, '$.dispatchDigest') = dispatch_digest AND
|
||||
json_extract(receipt_json, '$.approvalRequestId') = approval_request_id AND
|
||||
json_extract(receipt_json, '$.actionRef') = action_ref AND
|
||||
json_extract(receipt_json, '$.projectId') = project_id AND
|
||||
json_extract(receipt_json, '$.packageName') = package_name AND
|
||||
json_extract(receipt_json, '$.installationId') = installation_id AND
|
||||
json_extract(receipt_json, '$.lockDigest') = lock_digest AND
|
||||
json_extract(receipt_json, '$.recordDigest') = record_digest AND
|
||||
json_extract(receipt_json, '$.mutationId') = mutation_id AND
|
||||
json_extract(receipt_json, '$.mutationDigest') = mutation_digest AND
|
||||
json_extract(receipt_json, '$.auditEventId') = audit_event_id AND
|
||||
json_extract(receipt_json, '$.admittedAtMs') = admitted_at_ms AND
|
||||
json_extract(receipt_json, '$.receiptDigest') = receipt_digest
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_admission_time_check
|
||||
CHECK (admitted_at_ms >= 0)
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_admission_install_uidx ON "QingLong3PluginPackageAdmissionReceipts" (installation_id)`,
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_admission_audit_uidx ON "QingLong3PluginPackageAdmissionReceipts" (audit_event_id)`,
|
||||
`CREATE INDEX ql3_plugin_package_admission_project_idx ON "QingLong3PluginPackageAdmissionReceipts" (project_id, admitted_at_ms, dispatch_id)`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
const PREVIOUS_CAPABILITIES =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1}';
|
||||
const NEXT_CAPABILITIES =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1}';
|
||||
|
||||
export const local0042CapabilityV21Migration = defineLocalSqliteMigration({
|
||||
id: '0042-capability-v21',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 21,
|
||||
migration_id = '0041-plugin-package-admission-receipts',
|
||||
capabilities = '${NEXT_CAPABILITIES}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 20
|
||||
AND migration_id = '0039-approved-actions'
|
||||
AND capabilities = '${PREVIOUS_CAPABILITIES}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0043ApprovedActionExecutionsAndPackageProposalsMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0043-approved-action-executions-and-package-proposals',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3PluginPackageInstallProposals" (
|
||||
action_ref TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL,
|
||||
action_type TEXT NOT NULL,
|
||||
permission TEXT NOT NULL,
|
||||
action_digest TEXT NOT NULL,
|
||||
preview_digest TEXT NOT NULL,
|
||||
proposed_by_type TEXT NOT NULL,
|
||||
proposed_by_id TEXT NOT NULL,
|
||||
fence_project_version INTEGER NOT NULL,
|
||||
fence_binding_version INTEGER,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
proposal_json TEXT NOT NULL,
|
||||
proposal_digest TEXT NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_proposal_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "QingLong3Projects" (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_proposal_identity_check CHECK (
|
||||
length(action_ref) BETWEEN 1 AND 255 AND
|
||||
action_type = 'plugin_package.install' AND
|
||||
permission = 'package.manage' AND
|
||||
proposed_by_type IN
|
||||
('user','api_app','mcp_client','agent','system','worker') AND
|
||||
length(proposed_by_id) BETWEEN 1 AND 255
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_proposal_digest_check CHECK (
|
||||
length(action_digest) = 64 AND action_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(preview_digest) = 64 AND preview_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(proposal_digest) = 64 AND
|
||||
proposal_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_proposal_json_check CHECK (
|
||||
length(proposal_json) BETWEEN 2 AND 262144 AND
|
||||
json_valid(proposal_json) AND json_type(proposal_json) = 'object' AND
|
||||
json_extract(proposal_json, '$.schema') =
|
||||
'qinglong/plugin-package-install-proposal@v1' AND
|
||||
json_extract(proposal_json, '$.actionRef') = action_ref AND
|
||||
json_extract(proposal_json, '$.projectId') = project_id AND
|
||||
json_extract(proposal_json, '$.actionType') = action_type AND
|
||||
json_extract(proposal_json, '$.permission') = permission AND
|
||||
json_extract(proposal_json, '$.actionDigest') = action_digest AND
|
||||
json_extract(proposal_json, '$.previewDigest') = preview_digest AND
|
||||
json_extract(proposal_json, '$.proposedBy.type') = proposed_by_type AND
|
||||
json_extract(proposal_json, '$.proposedBy.id') = proposed_by_id AND
|
||||
json_extract(proposal_json, '$.proposalFence.projectVersion') =
|
||||
fence_project_version AND
|
||||
json_extract(proposal_json, '$.proposalFence.bindingVersion')
|
||||
IS fence_binding_version AND
|
||||
json_extract(proposal_json, '$.createdAtMs') = created_at_ms AND
|
||||
json_extract(proposal_json, '$.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
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE INDEX ql3_plugin_package_proposal_project_idx ON "QingLong3PluginPackageInstallProposals" (project_id, created_at_ms, action_ref)`,
|
||||
`
|
||||
CREATE TABLE "QingLong3ApprovedActionExecutions" (
|
||||
dispatch_id TEXT PRIMARY KEY,
|
||||
dispatch_digest TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
version INTEGER NOT NULL,
|
||||
attempt_count INTEGER NOT NULL,
|
||||
max_attempts INTEGER NOT NULL,
|
||||
eligible_at_ms INTEGER,
|
||||
next_attempt_at_ms INTEGER,
|
||||
lease_owner TEXT,
|
||||
lease_token TEXT,
|
||||
lease_expires_at_ms INTEGER,
|
||||
started_at_ms INTEGER,
|
||||
result_mutation_id TEXT,
|
||||
result_code TEXT,
|
||||
result_digest TEXT,
|
||||
completed_at_ms INTEGER,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
execution_json TEXT NOT NULL,
|
||||
execution_digest TEXT NOT NULL,
|
||||
CONSTRAINT ql3_approved_action_execution_dispatch_fk
|
||||
FOREIGN KEY (dispatch_id)
|
||||
REFERENCES "QingLong3ApprovedActionDispatches" (dispatch_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_approved_action_execution_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "QingLong3Projects" (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
|
||||
(length(lease_owner) BETWEEN 1 AND 128 AND
|
||||
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
|
||||
(length(result_mutation_id) BETWEEN 1 AND 128 AND
|
||||
length(result_code) BETWEEN 1 AND 64)
|
||||
),
|
||||
CONSTRAINT ql3_approved_action_execution_digest_check CHECK (
|
||||
length(dispatch_digest) = 64 AND
|
||||
dispatch_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
(result_digest IS NULL OR
|
||||
(length(result_digest) = 64 AND
|
||||
result_digest NOT GLOB '*[^0-9a-f]*')) AND
|
||||
length(execution_digest) = 64 AND
|
||||
execution_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_approved_action_execution_json_check CHECK (
|
||||
length(execution_json) BETWEEN 2 AND 65536 AND
|
||||
json_valid(execution_json) AND json_type(execution_json) = 'object' AND
|
||||
json_extract(execution_json, '$.schema') =
|
||||
'qinglong/approved-action-execution@v1' AND
|
||||
json_extract(execution_json, '$.dispatchId') = dispatch_id AND
|
||||
json_extract(execution_json, '$.dispatchDigest') = dispatch_digest AND
|
||||
json_extract(execution_json, '$.projectId') = project_id AND
|
||||
json_extract(execution_json, '$.status') = status AND
|
||||
json_extract(execution_json, '$.version') = version AND
|
||||
json_extract(execution_json, '$.attemptCount') = attempt_count AND
|
||||
json_extract(execution_json, '$.maxAttempts') = max_attempts AND
|
||||
json_extract(execution_json, '$.executionDigest') = execution_digest
|
||||
),
|
||||
CONSTRAINT ql3_approved_action_execution_time_check CHECK (
|
||||
created_at_ms >= 0 AND updated_at_ms >= created_at_ms
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE INDEX ql3_approved_action_execution_due_idx ON "QingLong3ApprovedActionExecutions" (eligible_at_ms, dispatch_id) WHERE status IN ('pending','leased','retry_wait')`,
|
||||
`CREATE INDEX ql3_approved_action_execution_recovery_idx ON "QingLong3ApprovedActionExecutions" (lease_expires_at_ms, dispatch_id) WHERE status = 'executing'`,
|
||||
`CREATE INDEX ql3_approved_action_execution_project_idx ON "QingLong3ApprovedActionExecutions" (project_id, updated_at_ms, dispatch_id)`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
const PREVIOUS_CAPABILITIES =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1}';
|
||||
const NEXT_CAPABILITIES =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1}';
|
||||
|
||||
export const local0044CapabilityV22Migration = defineLocalSqliteMigration({
|
||||
id: '0044-capability-v22',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 22,
|
||||
migration_id =
|
||||
'0043-approved-action-executions-and-package-proposals',
|
||||
capabilities = '${NEXT_CAPABILITIES}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 21
|
||||
AND migration_id = '0041-plugin-package-admission-receipts'
|
||||
AND capabilities = '${PREVIOUS_CAPABILITIES}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0045PluginPackageMaterializedRevisionsMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0045-plugin-package-materialized-revisions',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3PluginPackageMaterializedRevisions" (
|
||||
generation_digest TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL,
|
||||
package_name TEXT NOT NULL,
|
||||
generation INTEGER NOT NULL,
|
||||
lock_digest TEXT NOT NULL,
|
||||
manifest_digest TEXT NOT NULL,
|
||||
revision_digest TEXT NOT NULL,
|
||||
revision_json TEXT NOT NULL,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_materialized_revision_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "QingLong3Projects" (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_materialized_revision_identity_check CHECK (
|
||||
length(package_name) BETWEEN 1 AND 63 AND
|
||||
generation BETWEEN 1 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_materialized_revision_digest_check CHECK (
|
||||
length(generation_digest) = 64 AND
|
||||
generation_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(lock_digest) = 64 AND
|
||||
lock_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(manifest_digest) = 64 AND
|
||||
manifest_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(revision_digest) = 64 AND
|
||||
revision_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_materialized_revision_json_check CHECK (
|
||||
length(revision_json) BETWEEN 2 AND 25165824 AND
|
||||
json_valid(revision_json) AND json_type(revision_json) = 'object' AND
|
||||
json_extract(revision_json, '$.schema') =
|
||||
'qinglong/plugin-package-materialized-revision@v1' AND
|
||||
json_extract(revision_json, '$.generation.generationDigest') =
|
||||
generation_digest AND
|
||||
json_extract(revision_json, '$.generation.projectId') = project_id AND
|
||||
json_extract(revision_json, '$.generation.packageName') = package_name AND
|
||||
json_extract(revision_json, '$.generation.generation') = generation AND
|
||||
json_extract(revision_json, '$.generation.lockDigest') = lock_digest AND
|
||||
json_extract(revision_json, '$.manifestDigest') = manifest_digest AND
|
||||
json_extract(revision_json, '$.revisionDigest') = revision_digest
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_materialized_revision_time_check CHECK (
|
||||
created_at_ms >= 0
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_materialized_revision_generation_uidx ON "QingLong3PluginPackageMaterializedRevisions" (project_id, package_name, generation)`,
|
||||
`CREATE INDEX ql3_plugin_package_materialized_revision_lock_idx ON "QingLong3PluginPackageMaterializedRevisions" (lock_digest, generation_digest)`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
const PREVIOUS_CAPABILITIES =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1}';
|
||||
const NEXT_CAPABILITIES =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1}';
|
||||
|
||||
export const local0046CapabilityV23Migration = defineLocalSqliteMigration({
|
||||
id: '0046-capability-v23',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 23,
|
||||
migration_id = '0045-plugin-package-materialized-revisions',
|
||||
capabilities = '${NEXT_CAPABILITIES}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 22
|
||||
AND migration_id =
|
||||
'0043-approved-action-executions-and-package-proposals'
|
||||
AND capabilities = '${PREVIOUS_CAPABILITIES}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0047PluginPackageTaskReconciliationsMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0047-plugin-package-task-reconciliations',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3PluginPackageTaskOwnerships" (
|
||||
project_id TEXT NOT NULL,
|
||||
task_id TEXT NOT NULL,
|
||||
package_name TEXT NOT NULL,
|
||||
claimed_generation_digest TEXT NOT NULL,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
PRIMARY KEY (project_id, task_id),
|
||||
CONSTRAINT ql3_plugin_package_task_ownership_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "QingLong3Projects" (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_task_ownership_task_fk
|
||||
FOREIGN KEY (project_id, task_id)
|
||||
REFERENCES "QingLong3TaskDefinitions" (project_id, task_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_task_ownership_identity_check CHECK (
|
||||
length(task_id) BETWEEN 1 AND 128 AND
|
||||
length(package_name) BETWEEN 1 AND 63
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_task_ownership_digest_check CHECK (
|
||||
length(claimed_generation_digest) = 64 AND
|
||||
claimed_generation_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
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
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE INDEX ql3_plugin_package_task_ownership_package_idx ON "QingLong3PluginPackageTaskOwnerships" (project_id, package_name, task_id)`,
|
||||
`
|
||||
CREATE TABLE "QingLong3PluginPackageTaskReconciliations" (
|
||||
generation_digest TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL,
|
||||
package_name TEXT NOT NULL,
|
||||
generation INTEGER NOT NULL,
|
||||
materialized_revision_digest TEXT NOT NULL,
|
||||
lock_digest TEXT NOT NULL,
|
||||
previous_lock_digest TEXT,
|
||||
receipt_digest TEXT NOT NULL,
|
||||
receipt_json TEXT NOT NULL,
|
||||
committed_at_ms INTEGER NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_task_reconciliation_materialized_fk
|
||||
FOREIGN KEY (generation_digest)
|
||||
REFERENCES "QingLong3PluginPackageMaterializedRevisions" (generation_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_task_reconciliation_identity_check CHECK (
|
||||
length(package_name) BETWEEN 1 AND 63 AND
|
||||
generation BETWEEN 1 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_task_reconciliation_digest_check CHECK (
|
||||
length(generation_digest) = 64 AND
|
||||
generation_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(materialized_revision_digest) = 64 AND
|
||||
materialized_revision_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(lock_digest) = 64 AND lock_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
(previous_lock_digest IS NULL OR
|
||||
(length(previous_lock_digest) = 64 AND
|
||||
previous_lock_digest NOT GLOB '*[^0-9a-f]*')) AND
|
||||
length(receipt_digest) = 64 AND receipt_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_task_reconciliation_json_check CHECK (
|
||||
length(receipt_json) BETWEEN 2 AND 8388608 AND
|
||||
json_valid(receipt_json) AND json_type(receipt_json) = 'object' AND
|
||||
json_extract(receipt_json, '$.schema') =
|
||||
'qinglong/plugin-package-task-reconciliation@v1' AND
|
||||
json_extract(receipt_json, '$.generationDigest') = generation_digest AND
|
||||
json_extract(receipt_json, '$.projectId') = project_id AND
|
||||
json_extract(receipt_json, '$.packageName') = package_name AND
|
||||
json_extract(receipt_json, '$.generation') = generation AND
|
||||
json_extract(receipt_json, '$.materializedRevisionDigest') =
|
||||
materialized_revision_digest AND
|
||||
json_extract(receipt_json, '$.lockDigest') = lock_digest AND
|
||||
json_extract(receipt_json, '$.receiptDigest') = receipt_digest
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_task_reconciliation_time_check CHECK (
|
||||
committed_at_ms >= 0
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_task_reconciliation_generation_uidx ON "QingLong3PluginPackageTaskReconciliations" (project_id, package_name, generation)`,
|
||||
`CREATE INDEX ql3_plugin_package_task_reconciliation_lock_idx ON "QingLong3PluginPackageTaskReconciliations" (lock_digest, generation_digest)`,
|
||||
`
|
||||
CREATE TABLE "QingLong3PluginPackageTaskReconciliationItems" (
|
||||
generation_digest TEXT NOT NULL,
|
||||
task_id TEXT NOT NULL,
|
||||
revision INTEGER NOT NULL,
|
||||
disposition TEXT NOT NULL,
|
||||
content_digest TEXT NOT NULL,
|
||||
PRIMARY KEY (generation_digest, task_id),
|
||||
CONSTRAINT ql3_plugin_package_task_reconciliation_item_reconciliation_fk
|
||||
FOREIGN KEY (generation_digest)
|
||||
REFERENCES "QingLong3PluginPackageTaskReconciliations" (generation_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_task_reconciliation_item_identity_check CHECK (
|
||||
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 (
|
||||
length(content_digest) = 64 AND content_digest NOT GLOB '*[^0-9a-f]*'
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE INDEX ql3_plugin_package_task_reconciliation_item_task_idx ON "QingLong3PluginPackageTaskReconciliationItems" (task_id, generation_digest)`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
const PREVIOUS_CAPABILITIES =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1}';
|
||||
const NEXT_CAPABILITIES =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1}';
|
||||
|
||||
export const local0048CapabilityV24Migration = defineLocalSqliteMigration({
|
||||
id: '0048-capability-v24',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 24,
|
||||
migration_id = '0047-plugin-package-task-reconciliations',
|
||||
capabilities = '${NEXT_CAPABILITIES}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 23
|
||||
AND migration_id = '0045-plugin-package-materialized-revisions'
|
||||
AND capabilities = '${PREVIOUS_CAPABILITIES}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0049ProjectToolDefinitionSnapshotsMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0049-project-tool-definition-snapshots',
|
||||
statements: [
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_plugin_package_installs_snapshot_source_uidx
|
||||
ON "QingLong3PluginPackageInstalls" (
|
||||
project_id, package_name, installation_id, target_generation, lock_digest
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_plugin_package_materialized_revision_snapshot_source_uidx
|
||||
ON "QingLong3PluginPackageMaterializedRevisions" (
|
||||
project_id, package_name, generation, generation_digest,
|
||||
lock_digest, revision_digest
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE TABLE "QingLong3ProjectToolDefinitionSnapshots" (
|
||||
project_id TEXT NOT NULL,
|
||||
active_vector_digest TEXT NOT NULL,
|
||||
definitions_digest TEXT NOT NULL,
|
||||
snapshot_digest TEXT NOT NULL,
|
||||
snapshot_json TEXT NOT NULL,
|
||||
committed_at_ms INTEGER NOT NULL,
|
||||
PRIMARY KEY (project_id, active_vector_digest),
|
||||
CONSTRAINT ql3_project_tool_definition_snapshot_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "QingLong3Projects" (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_project_tool_definition_snapshot_identity_check CHECK (
|
||||
length(project_id) BETWEEN 1 AND 128
|
||||
),
|
||||
CONSTRAINT ql3_project_tool_definition_snapshot_digest_check CHECK (
|
||||
length(active_vector_digest) = 64 AND
|
||||
active_vector_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(definitions_digest) = 64 AND
|
||||
definitions_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(snapshot_digest) = 64 AND
|
||||
snapshot_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_project_tool_definition_snapshot_json_check CHECK (
|
||||
length(snapshot_json) BETWEEN 2 AND 8388608 AND
|
||||
json_valid(snapshot_json) AND json_type(snapshot_json) = 'object' AND
|
||||
json_extract(snapshot_json, '$.schema') =
|
||||
'qinglong/project-tool-definition-snapshot@v1' AND
|
||||
json_extract(snapshot_json, '$.projectId') = project_id AND
|
||||
json_extract(snapshot_json, '$.activeVectorDigest') =
|
||||
active_vector_digest AND
|
||||
json_extract(snapshot_json, '$.definitionsDigest') =
|
||||
definitions_digest AND
|
||||
json_extract(snapshot_json, '$.snapshotDigest') = snapshot_digest
|
||||
),
|
||||
CONSTRAINT ql3_project_tool_definition_snapshot_time_check CHECK (
|
||||
committed_at_ms >= 0
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_project_tool_definition_snapshot_digest_uidx ON "QingLong3ProjectToolDefinitionSnapshots" (snapshot_digest)`,
|
||||
`CREATE INDEX ql3_project_tool_definition_snapshot_current_idx ON "QingLong3ProjectToolDefinitionSnapshots" (project_id, committed_at_ms DESC, active_vector_digest)`,
|
||||
`
|
||||
CREATE TABLE "QingLong3ProjectToolDefinitionSnapshotSources" (
|
||||
project_id TEXT NOT NULL,
|
||||
active_vector_digest TEXT NOT NULL,
|
||||
package_name TEXT NOT NULL,
|
||||
installation_id TEXT NOT NULL,
|
||||
generation INTEGER NOT NULL,
|
||||
generation_digest TEXT NOT NULL,
|
||||
lock_digest TEXT NOT NULL,
|
||||
revision_digest TEXT NOT NULL,
|
||||
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 "QingLong3ProjectToolDefinitionSnapshots" (
|
||||
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 "QingLong3PluginPackageInstalls" (
|
||||
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 "QingLong3PluginPackageMaterializedRevisions" (
|
||||
project_id, package_name, generation, generation_digest,
|
||||
lock_digest, revision_digest
|
||||
)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_project_tool_definition_snapshot_source_identity_check
|
||||
CHECK (
|
||||
length(package_name) BETWEEN 1 AND 63 AND
|
||||
length(installation_id) BETWEEN 1 AND 128 AND
|
||||
generation BETWEEN 1 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_project_tool_definition_snapshot_source_digest_check CHECK (
|
||||
length(active_vector_digest) = 64 AND
|
||||
active_vector_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(generation_digest) = 64 AND
|
||||
generation_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(lock_digest) = 64 AND
|
||||
lock_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(revision_digest) = 64 AND
|
||||
revision_digest NOT GLOB '*[^0-9a-f]*'
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE INDEX ql3_project_tool_definition_snapshot_source_generation_idx ON "QingLong3ProjectToolDefinitionSnapshotSources" (generation_digest, project_id, package_name)`,
|
||||
`CREATE INDEX ql3_project_tool_definition_snapshot_source_install_idx ON "QingLong3ProjectToolDefinitionSnapshotSources" (installation_id, active_vector_digest)`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
const PREVIOUS_CAPABILITIES =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1}';
|
||||
const NEXT_CAPABILITIES =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1}';
|
||||
|
||||
export const local0050CapabilityV25Migration = defineLocalSqliteMigration({
|
||||
id: '0050-capability-v25',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 25,
|
||||
migration_id = '0049-project-tool-definition-snapshots',
|
||||
capabilities = '${NEXT_CAPABILITIES}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 24
|
||||
AND migration_id = '0047-plugin-package-task-reconciliations'
|
||||
AND capabilities = '${PREVIOUS_CAPABILITIES}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,222 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
import { LOCAL_STEP_RUN_REFERENCE_TRIGGERS } from '../run/stepRunSchemaContract';
|
||||
|
||||
export const local0051StepRunsMigration = defineLocalSqliteMigration({
|
||||
id: '0051-step-runs',
|
||||
statements: [
|
||||
`
|
||||
CREATE TEMP TABLE "QingLong3StepRunReferenceUpgradeGuard" (
|
||||
valid INTEGER NOT NULL CHECK (valid = 1)
|
||||
)
|
||||
`,
|
||||
`
|
||||
INSERT INTO "QingLong3StepRunReferenceUpgradeGuard" (valid)
|
||||
SELECT CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1 FROM "RunAttempts" WHERE step_run_id IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT 1 FROM "RunEvents" WHERE step_run_id IS NOT NULL
|
||||
) THEN 0
|
||||
ELSE 1
|
||||
END
|
||||
`,
|
||||
`DROP TABLE "QingLong3StepRunReferenceUpgradeGuard"`,
|
||||
`
|
||||
CREATE TABLE "StepRuns" (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
run_id TEXT NOT NULL,
|
||||
parent_step_run_id TEXT,
|
||||
step_key TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
definition_ref TEXT NOT NULL,
|
||||
definition_digest TEXT NOT NULL,
|
||||
required INTEGER NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
version INTEGER NOT NULL,
|
||||
attempt_count INTEGER NOT NULL,
|
||||
input_ref TEXT,
|
||||
output_ref TEXT,
|
||||
approval_request_id TEXT,
|
||||
ready_at_ms INTEGER,
|
||||
started_at_ms INTEGER,
|
||||
finished_at_ms INTEGER,
|
||||
result_code TEXT,
|
||||
error_summary TEXT,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
last_mutation_id TEXT NOT NULL,
|
||||
step_run_digest TEXT NOT NULL,
|
||||
step_run_json TEXT NOT NULL,
|
||||
CONSTRAINT ql3_step_runs_run_fk
|
||||
FOREIGN KEY (run_id) REFERENCES "Runs" (id) ON DELETE CASCADE,
|
||||
CONSTRAINT ql3_step_runs_parent_fk
|
||||
FOREIGN KEY (run_id, parent_step_run_id)
|
||||
REFERENCES "StepRuns" (run_id, id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_step_runs_identity_check CHECK (
|
||||
length(id) BETWEEN 1 AND 128 AND
|
||||
length(run_id) BETWEEN 1 AND 128 AND
|
||||
(parent_step_run_id IS NULL OR
|
||||
(length(parent_step_run_id) BETWEEN 1 AND 128 AND
|
||||
parent_step_run_id <> id)) AND
|
||||
length(step_key) BETWEEN 1 AND 128 AND
|
||||
length(CAST(definition_ref AS BLOB)) 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 (
|
||||
length(definition_digest) = 64 AND
|
||||
definition_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(step_run_digest) = 64 AND
|
||||
step_run_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_step_runs_counter_check CHECK (
|
||||
required IN (0, 1) AND
|
||||
version BETWEEN 1 AND 2147483647 AND
|
||||
attempt_count BETWEEN 0 AND 64
|
||||
),
|
||||
CONSTRAINT ql3_step_runs_reference_check CHECK (
|
||||
(input_ref IS NULL OR
|
||||
length(CAST(input_ref AS BLOB)) BETWEEN 1 AND 512) AND
|
||||
(output_ref IS NULL OR
|
||||
length(CAST(output_ref AS BLOB)) BETWEEN 1 AND 512) AND
|
||||
(approval_request_id IS NULL OR
|
||||
length(approval_request_id) BETWEEN 1 AND 128)
|
||||
),
|
||||
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_result_shape_check CHECK (
|
||||
(status = 'waiting_approval' AND approval_request_id IS NOT NULL) OR
|
||||
status <> 'waiting_approval'
|
||||
),
|
||||
CONSTRAINT ql3_step_runs_result_value_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
|
||||
(length(result_code) BETWEEN 1 AND 64 AND
|
||||
result_code NOT GLOB '*[^a-z0-9_]*' AND
|
||||
substr(result_code, 1, 1) GLOB '[a-z]')) AND
|
||||
(error_summary IS NULL OR
|
||||
length(CAST(error_summary AS BLOB)) BETWEEN 1 AND 2048)
|
||||
),
|
||||
CONSTRAINT ql3_step_runs_mutation_identity_check CHECK (
|
||||
length(last_mutation_id) BETWEEN 1 AND 128
|
||||
),
|
||||
CONSTRAINT ql3_step_runs_json_check CHECK (
|
||||
length(CAST(step_run_json AS BLOB)) BETWEEN 2 AND 16384 AND
|
||||
json_valid(step_run_json) AND json_type(step_run_json) = 'object' AND
|
||||
json_extract(step_run_json, '$.schema') = 'qinglong/step-run@v1' AND
|
||||
json_extract(step_run_json, '$.id') = id AND
|
||||
json_extract(step_run_json, '$.runId') = run_id AND
|
||||
json_extract(step_run_json, '$.parentStepRunId') IS parent_step_run_id AND
|
||||
json_extract(step_run_json, '$.stepKey') = step_key AND
|
||||
json_extract(step_run_json, '$.kind') = kind AND
|
||||
json_extract(step_run_json, '$.definitionRef') = definition_ref AND
|
||||
json_extract(step_run_json, '$.definitionDigest') = definition_digest AND
|
||||
json_extract(step_run_json, '$.required') IS required AND
|
||||
json_extract(step_run_json, '$.status') = status AND
|
||||
json_extract(step_run_json, '$.version') = version AND
|
||||
json_extract(step_run_json, '$.attemptCount') IS attempt_count AND
|
||||
json_extract(step_run_json, '$.inputRef') IS input_ref AND
|
||||
json_extract(step_run_json, '$.outputRef') IS output_ref AND
|
||||
json_extract(step_run_json, '$.approvalRequestId') IS
|
||||
approval_request_id AND
|
||||
json_extract(step_run_json, '$.readyAtMs') IS ready_at_ms AND
|
||||
json_extract(step_run_json, '$.startedAtMs') IS started_at_ms AND
|
||||
json_extract(step_run_json, '$.finishedAtMs') IS finished_at_ms AND
|
||||
json_extract(step_run_json, '$.resultCode') IS result_code AND
|
||||
json_extract(step_run_json, '$.errorSummary') IS error_summary AND
|
||||
json_extract(step_run_json, '$.createdAtMs') IS created_at_ms AND
|
||||
json_extract(step_run_json, '$.updatedAtMs') IS updated_at_ms AND
|
||||
json_extract(step_run_json, '$.lastMutationId') = last_mutation_id AND
|
||||
json_extract(step_run_json, '$.stepRunDigest') = step_run_digest
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_step_runs_run_id_uidx ON "StepRuns" (run_id, id)`,
|
||||
`CREATE UNIQUE INDEX ql3_step_runs_run_step_uidx ON "StepRuns" (run_id, step_key)`,
|
||||
`CREATE INDEX ql3_step_runs_run_status_idx ON "StepRuns" (run_id, status, id)`,
|
||||
`CREATE INDEX ql3_step_runs_recovery_idx ON "StepRuns" (status, updated_at_ms, id) WHERE status IN ('waiting_approval', 'running', 'lost')`,
|
||||
`
|
||||
CREATE TABLE "StepRunMutations" (
|
||||
mutation_id TEXT PRIMARY KEY NOT NULL,
|
||||
mutation_digest TEXT NOT NULL,
|
||||
run_id TEXT NOT NULL,
|
||||
step_run_id TEXT NOT NULL,
|
||||
step_run_digest TEXT NOT NULL,
|
||||
event_id TEXT NOT NULL,
|
||||
event_sequence INTEGER NOT NULL,
|
||||
run_version INTEGER NOT NULL,
|
||||
step_run_json TEXT NOT NULL,
|
||||
committed_at_ms INTEGER NOT NULL,
|
||||
CONSTRAINT ql3_step_run_mutations_step_fk
|
||||
FOREIGN KEY (run_id, step_run_id)
|
||||
REFERENCES "StepRuns" (run_id, id) ON DELETE CASCADE,
|
||||
CONSTRAINT ql3_step_run_mutations_event_fk
|
||||
FOREIGN KEY (event_id) REFERENCES "RunEvents" (id) ON DELETE CASCADE,
|
||||
CONSTRAINT ql3_step_run_mutations_identity_check CHECK (
|
||||
length(mutation_id) BETWEEN 1 AND 128 AND
|
||||
length(run_id) BETWEEN 1 AND 128 AND
|
||||
length(step_run_id) BETWEEN 1 AND 128 AND
|
||||
length(event_id) BETWEEN 1 AND 128
|
||||
),
|
||||
CONSTRAINT ql3_step_run_mutations_digest_check CHECK (
|
||||
length(mutation_digest) = 64 AND
|
||||
mutation_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(step_run_digest) = 64 AND
|
||||
step_run_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
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 (
|
||||
length(CAST(step_run_json AS BLOB)) BETWEEN 2 AND 16384 AND
|
||||
json_valid(step_run_json) AND json_type(step_run_json) = 'object' AND
|
||||
json_extract(step_run_json, '$.schema') = 'qinglong/step-run@v1' AND
|
||||
json_extract(step_run_json, '$.id') = step_run_id AND
|
||||
json_extract(step_run_json, '$.runId') = run_id AND
|
||||
json_extract(step_run_json, '$.lastMutationId') = mutation_id AND
|
||||
json_extract(step_run_json, '$.stepRunDigest') = step_run_digest
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_step_run_mutations_event_uidx ON "StepRunMutations" (event_id)`,
|
||||
`CREATE INDEX ql3_step_run_mutations_step_idx ON "StepRunMutations" (run_id, step_run_id, event_sequence, mutation_id)`,
|
||||
...LOCAL_STEP_RUN_REFERENCE_TRIGGERS.map((trigger) => trigger.sql),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
const PREVIOUS_CAPABILITIES =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1}';
|
||||
const NEXT_CAPABILITIES =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1}';
|
||||
|
||||
export const local0052CapabilityV26Migration = defineLocalSqliteMigration({
|
||||
id: '0052-capability-v26',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 26,
|
||||
migration_id = '0051-step-runs',
|
||||
capabilities = '${NEXT_CAPABILITIES}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 25
|
||||
AND migration_id = '0049-project-tool-definition-snapshots'
|
||||
AND capabilities = '${PREVIOUS_CAPABILITIES}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0053ToolExecutionEvidenceMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0053-tool-execution-evidence',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ToolExecutionTraceAnchors" (
|
||||
trace_id TEXT NOT NULL,
|
||||
span_id TEXT NOT NULL,
|
||||
parent_span_id TEXT,
|
||||
project_id TEXT NOT NULL,
|
||||
run_id TEXT NOT NULL,
|
||||
step_run_id TEXT NOT NULL,
|
||||
invocation_plan_digest TEXT NOT NULL,
|
||||
binding_digest TEXT NOT NULL,
|
||||
adapter_digest TEXT NOT NULL,
|
||||
redaction_contract_digest TEXT NOT NULL,
|
||||
audit_contract_digest TEXT NOT NULL,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
trace_digest TEXT NOT NULL,
|
||||
trace_json TEXT 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 "StepRuns" (run_id, id) ON DELETE CASCADE,
|
||||
CONSTRAINT ql3_tool_execution_trace_identity_check CHECK (
|
||||
length(trace_id) = 32 AND trace_id NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(span_id) = 16 AND span_id NOT GLOB '*[^0-9a-f]*' AND
|
||||
(parent_span_id IS NULL OR (
|
||||
length(parent_span_id) = 16 AND
|
||||
parent_span_id NOT GLOB '*[^0-9a-f]*' AND
|
||||
parent_span_id <> span_id
|
||||
)) AND
|
||||
length(project_id) BETWEEN 1 AND 128 AND
|
||||
length(run_id) BETWEEN 1 AND 128 AND
|
||||
length(step_run_id) BETWEEN 1 AND 128
|
||||
),
|
||||
CONSTRAINT ql3_tool_execution_trace_digest_check CHECK (
|
||||
length(invocation_plan_digest) = 64 AND
|
||||
invocation_plan_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(binding_digest) = 64 AND
|
||||
binding_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(adapter_digest) = 64 AND
|
||||
adapter_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(redaction_contract_digest) = 64 AND
|
||||
redaction_contract_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(audit_contract_digest) = 64 AND
|
||||
audit_contract_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(trace_digest) = 64 AND
|
||||
trace_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_tool_execution_trace_time_check CHECK (
|
||||
created_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_tool_execution_trace_json_check CHECK (
|
||||
length(CAST(trace_json AS BLOB)) BETWEEN 2 AND 16384 AND
|
||||
json_valid(trace_json) AND json_type(trace_json) = 'object' AND
|
||||
json_extract(trace_json, '$.schema') =
|
||||
'qinglong/tool-execution-trace-anchor@v1' AND
|
||||
json_extract(trace_json, '$.traceId') = trace_id AND
|
||||
json_extract(trace_json, '$.spanId') = span_id AND
|
||||
json_extract(trace_json, '$.parentSpanId') IS parent_span_id AND
|
||||
json_extract(trace_json, '$.projectId') = project_id AND
|
||||
json_extract(trace_json, '$.runId') = run_id AND
|
||||
json_extract(trace_json, '$.stepRunId') = step_run_id AND
|
||||
json_extract(trace_json, '$.invocationPlanDigest') =
|
||||
invocation_plan_digest AND
|
||||
json_extract(trace_json, '$.bindingDigest') = binding_digest AND
|
||||
json_extract(trace_json, '$.adapterDigest') = adapter_digest AND
|
||||
json_extract(trace_json, '$.redactionContractDigest') =
|
||||
redaction_contract_digest AND
|
||||
json_extract(trace_json, '$.auditContractDigest') =
|
||||
audit_contract_digest AND
|
||||
json_extract(trace_json, '$.createdAtMs') IS created_at_ms AND
|
||||
json_extract(trace_json, '$.traceDigest') = trace_digest
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE INDEX ql3_tool_execution_trace_run_idx ON "ToolExecutionTraceAnchors" (run_id, created_at_ms, trace_id, span_id)`,
|
||||
`CREATE INDEX ql3_tool_execution_trace_step_idx ON "ToolExecutionTraceAnchors" (run_id, step_run_id, created_at_ms, trace_id, span_id)`,
|
||||
`
|
||||
CREATE TABLE "ToolExecutionAuditReceipts" (
|
||||
event_id TEXT PRIMARY KEY NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
run_id TEXT NOT NULL,
|
||||
step_run_id TEXT NOT NULL,
|
||||
trace_id TEXT NOT NULL,
|
||||
span_id TEXT NOT NULL,
|
||||
trace_digest TEXT NOT NULL,
|
||||
invocation_plan_digest TEXT NOT NULL,
|
||||
binding_digest TEXT NOT NULL,
|
||||
audit_record_digest TEXT NOT NULL,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
receipt_digest TEXT NOT NULL,
|
||||
audit_json TEXT NOT NULL,
|
||||
receipt_json TEXT NOT NULL,
|
||||
CONSTRAINT ql3_tool_execution_audit_event_fk
|
||||
FOREIGN KEY (event_id)
|
||||
REFERENCES "QingLong3SecurityAuditEvents" (event_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_execution_audit_trace_fk
|
||||
FOREIGN KEY (trace_id, span_id)
|
||||
REFERENCES "ToolExecutionTraceAnchors" (trace_id, span_id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT ql3_tool_execution_audit_step_fk
|
||||
FOREIGN KEY (run_id, step_run_id)
|
||||
REFERENCES "StepRuns" (run_id, id) ON DELETE CASCADE,
|
||||
CONSTRAINT ql3_tool_execution_audit_identity_check CHECK (
|
||||
length(event_id) BETWEEN 1 AND 128 AND
|
||||
length(project_id) BETWEEN 1 AND 128 AND
|
||||
length(run_id) BETWEEN 1 AND 128 AND
|
||||
length(step_run_id) BETWEEN 1 AND 128 AND
|
||||
length(trace_id) = 32 AND trace_id NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(span_id) = 16 AND span_id NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_tool_execution_audit_digest_check CHECK (
|
||||
length(trace_digest) = 64 AND
|
||||
trace_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(invocation_plan_digest) = 64 AND
|
||||
invocation_plan_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(binding_digest) = 64 AND
|
||||
binding_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(audit_record_digest) = 64 AND
|
||||
audit_record_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(receipt_digest) = 64 AND
|
||||
receipt_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_tool_execution_audit_time_check CHECK (
|
||||
created_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_tool_execution_audit_json_check CHECK (
|
||||
length(CAST(audit_json AS BLOB)) BETWEEN 2 AND 8192 AND
|
||||
json_valid(audit_json) AND json_type(audit_json) = 'object' AND
|
||||
json_extract(audit_json, '$.eventId') = event_id AND
|
||||
json_extract(audit_json, '$.projectId') = project_id AND
|
||||
json_extract(audit_json, '$.operationId') = 'tool.invoke.start' AND
|
||||
json_extract(audit_json, '$.outcome') = 'allowed' AND
|
||||
json_type(audit_json, '$.fence') = 'object' AND
|
||||
json_extract(audit_json, '$.occurredAtMs') IS created_at_ms
|
||||
),
|
||||
CONSTRAINT ql3_tool_execution_audit_receipt_json_check CHECK (
|
||||
length(CAST(receipt_json AS BLOB)) BETWEEN 2 AND 16384 AND
|
||||
json_valid(receipt_json) AND json_type(receipt_json) = 'object' AND
|
||||
json_extract(receipt_json, '$.schema') =
|
||||
'qinglong/tool-execution-audit-receipt@v1' AND
|
||||
json_extract(receipt_json, '$.eventId') = event_id AND
|
||||
json_extract(receipt_json, '$.projectId') = project_id AND
|
||||
json_extract(receipt_json, '$.runId') = run_id AND
|
||||
json_extract(receipt_json, '$.stepRunId') = step_run_id AND
|
||||
json_extract(receipt_json, '$.traceId') = trace_id AND
|
||||
json_extract(receipt_json, '$.spanId') = span_id AND
|
||||
json_extract(receipt_json, '$.traceDigest') = trace_digest AND
|
||||
json_extract(receipt_json, '$.invocationPlanDigest') =
|
||||
invocation_plan_digest AND
|
||||
json_extract(receipt_json, '$.bindingDigest') = binding_digest AND
|
||||
json_extract(receipt_json, '$.auditRecordDigest') =
|
||||
audit_record_digest AND
|
||||
json_extract(receipt_json, '$.createdAtMs') IS created_at_ms AND
|
||||
json_extract(receipt_json, '$.receiptDigest') = receipt_digest
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_tool_execution_audit_trace_uidx ON "ToolExecutionAuditReceipts" (trace_id, span_id)`,
|
||||
`CREATE INDEX ql3_tool_execution_audit_run_idx ON "ToolExecutionAuditReceipts" (run_id, created_at_ms, trace_id, span_id)`,
|
||||
`CREATE INDEX ql3_tool_execution_audit_step_idx ON "ToolExecutionAuditReceipts" (run_id, step_run_id, created_at_ms, event_id)`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
const PREVIOUS_CAPABILITIES =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1}';
|
||||
const NEXT_CAPABILITIES =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1}';
|
||||
|
||||
export const local0054CapabilityV27Migration = defineLocalSqliteMigration({
|
||||
id: '0054-capability-v27',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 27,
|
||||
migration_id = '0053-tool-execution-evidence',
|
||||
capabilities = '${NEXT_CAPABILITIES}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 26
|
||||
AND migration_id = '0051-step-runs'
|
||||
AND capabilities = '${PREVIOUS_CAPABILITIES}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0055ToolExecutionStartBarriersMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0055-tool-execution-start-barriers',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ToolExecutionStartBarriers" (
|
||||
start_id TEXT PRIMARY KEY NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
run_id TEXT NOT NULL,
|
||||
step_run_id TEXT NOT NULL,
|
||||
started_step_run_version INTEGER NOT NULL,
|
||||
step_run_mutation_id TEXT NOT NULL,
|
||||
run_event_id TEXT NOT NULL,
|
||||
trace_id TEXT NOT NULL,
|
||||
span_id TEXT NOT NULL,
|
||||
audit_event_id TEXT NOT NULL,
|
||||
command_digest TEXT NOT NULL,
|
||||
barrier_digest TEXT NOT NULL,
|
||||
started_at_ms INTEGER NOT NULL,
|
||||
barrier_json TEXT NOT NULL,
|
||||
CONSTRAINT ql3_tool_start_step_fk
|
||||
FOREIGN KEY (run_id, step_run_id)
|
||||
REFERENCES "StepRuns" (run_id, id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_start_mutation_fk
|
||||
FOREIGN KEY (step_run_mutation_id)
|
||||
REFERENCES "StepRunMutations" (mutation_id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_start_event_fk
|
||||
FOREIGN KEY (run_event_id)
|
||||
REFERENCES "RunEvents" (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_start_trace_fk
|
||||
FOREIGN KEY (trace_id, span_id)
|
||||
REFERENCES "ToolExecutionTraceAnchors" (trace_id, span_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_start_audit_fk
|
||||
FOREIGN KEY (audit_event_id)
|
||||
REFERENCES "ToolExecutionAuditReceipts" (event_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_start_identity_check CHECK (
|
||||
length(start_id) BETWEEN 1 AND 128 AND
|
||||
length(project_id) BETWEEN 1 AND 128 AND
|
||||
length(run_id) BETWEEN 1 AND 128 AND
|
||||
length(step_run_id) BETWEEN 1 AND 128 AND
|
||||
length(step_run_mutation_id) BETWEEN 1 AND 128 AND
|
||||
length(run_event_id) BETWEEN 1 AND 128 AND
|
||||
length(trace_id) = 32 AND trace_id NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(span_id) = 16 AND span_id NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(audit_event_id) BETWEEN 1 AND 128
|
||||
),
|
||||
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 (
|
||||
length(command_digest) = 64 AND
|
||||
command_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(barrier_digest) = 64 AND
|
||||
barrier_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_tool_start_json_check CHECK (
|
||||
length(CAST(barrier_json AS BLOB)) BETWEEN 2 AND 16384 AND
|
||||
json_valid(barrier_json) AND json_type(barrier_json) = 'object' AND
|
||||
json_extract(barrier_json, '$.schema') =
|
||||
'qinglong/tool-execution-start-barrier@v1' AND
|
||||
json_extract(barrier_json, '$.startId') = start_id AND
|
||||
json_extract(barrier_json, '$.projectId') = project_id AND
|
||||
json_extract(barrier_json, '$.runId') = run_id AND
|
||||
json_extract(barrier_json, '$.stepRunId') = step_run_id AND
|
||||
json_extract(barrier_json, '$.startedStepRunVersion') IS
|
||||
started_step_run_version AND
|
||||
json_extract(barrier_json, '$.stepRunMutationId') =
|
||||
step_run_mutation_id AND
|
||||
json_extract(barrier_json, '$.runEventId') = run_event_id AND
|
||||
json_extract(barrier_json, '$.traceId') = trace_id AND
|
||||
json_extract(barrier_json, '$.spanId') = span_id AND
|
||||
json_extract(barrier_json, '$.auditEventId') = audit_event_id AND
|
||||
json_extract(barrier_json, '$.commandDigest') = command_digest AND
|
||||
json_extract(barrier_json, '$.barrierDigest') = barrier_digest AND
|
||||
json_extract(barrier_json, '$.startedAtMs') IS started_at_ms
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_tool_start_step_version_uidx ON "ToolExecutionStartBarriers" (run_id, step_run_id, started_step_run_version)`,
|
||||
`CREATE UNIQUE INDEX ql3_tool_start_mutation_uidx ON "ToolExecutionStartBarriers" (step_run_mutation_id)`,
|
||||
`CREATE UNIQUE INDEX ql3_tool_start_event_uidx ON "ToolExecutionStartBarriers" (run_event_id)`,
|
||||
`CREATE UNIQUE INDEX ql3_tool_start_trace_uidx ON "ToolExecutionStartBarriers" (trace_id, span_id)`,
|
||||
`CREATE UNIQUE INDEX ql3_tool_start_audit_uidx ON "ToolExecutionStartBarriers" (audit_event_id)`,
|
||||
`CREATE INDEX ql3_tool_start_run_time_idx ON "ToolExecutionStartBarriers" (run_id, started_at_ms, start_id)`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
const PREVIOUS_CAPABILITIES =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1}';
|
||||
const NEXT_CAPABILITIES =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1}';
|
||||
|
||||
export const local0056CapabilityV28Migration = defineLocalSqliteMigration({
|
||||
id: '0056-capability-v28',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 28,
|
||||
migration_id = '0055-tool-execution-start-barriers',
|
||||
capabilities = '${NEXT_CAPABILITIES}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 27
|
||||
AND migration_id = '0053-tool-execution-evidence'
|
||||
AND capabilities = '${PREVIOUS_CAPABILITIES}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0057ToolInvocationArtifactsMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0057-tool-invocation-artifacts',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ToolInvocationInputArtifacts" (
|
||||
artifact_id TEXT PRIMARY KEY NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
action_ref TEXT NOT NULL,
|
||||
input_digest TEXT NOT NULL,
|
||||
invocation_action_digest TEXT NOT NULL,
|
||||
artifact_digest TEXT NOT NULL,
|
||||
key_id TEXT NOT NULL,
|
||||
algorithm TEXT NOT NULL,
|
||||
plaintext_bytes INTEGER NOT NULL,
|
||||
sealed_at_ms INTEGER NOT NULL,
|
||||
artifact_json TEXT NOT NULL,
|
||||
CONSTRAINT ql3_tool_input_artifact_project_fk
|
||||
FOREIGN KEY (project_id)
|
||||
REFERENCES "QingLong3Projects" (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_input_artifact_identity_check CHECK (
|
||||
length(artifact_id) BETWEEN 1 AND 128 AND
|
||||
length(project_id) BETWEEN 1 AND 128 AND
|
||||
length(action_ref) BETWEEN 1 AND 255 AND
|
||||
length(key_id) BETWEEN 1 AND 128 AND
|
||||
algorithm = 'aes-256-gcm'
|
||||
),
|
||||
CONSTRAINT ql3_tool_input_artifact_digest_check CHECK (
|
||||
length(input_digest) = 64 AND
|
||||
input_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(invocation_action_digest) = 64 AND
|
||||
invocation_action_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(artifact_digest) = 64 AND
|
||||
artifact_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
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 (
|
||||
length(CAST(artifact_json AS BLOB)) BETWEEN 2 AND 98304 AND
|
||||
json_valid(artifact_json) AND json_type(artifact_json) = 'object' AND
|
||||
json_extract(artifact_json, '$.schema') =
|
||||
'qinglong/tool-invocation-input-artifact@v1' AND
|
||||
json_extract(artifact_json, '$.artifactId') = artifact_id AND
|
||||
json_extract(artifact_json, '$.projectId') = project_id AND
|
||||
json_extract(artifact_json, '$.actionRef') = action_ref AND
|
||||
json_extract(artifact_json, '$.inputDigest') = input_digest AND
|
||||
json_extract(artifact_json, '$.invocationActionDigest') =
|
||||
invocation_action_digest AND
|
||||
json_extract(artifact_json, '$.artifactDigest') = artifact_digest AND
|
||||
json_extract(artifact_json, '$.keyId') = key_id AND
|
||||
json_extract(artifact_json, '$.algorithm') = algorithm AND
|
||||
json_extract(artifact_json, '$.plaintextBytes') IS plaintext_bytes AND
|
||||
json_extract(artifact_json, '$.sealedAtMs') IS sealed_at_ms
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_tool_input_artifact_action_uidx ON "ToolInvocationInputArtifacts" (project_id, action_ref)`,
|
||||
`CREATE INDEX ql3_tool_input_artifact_project_time_idx ON "ToolInvocationInputArtifacts" (project_id, sealed_at_ms, artifact_id)`,
|
||||
`
|
||||
CREATE TABLE "ToolInvocationPreviewArtifacts" (
|
||||
artifact_id TEXT PRIMARY KEY NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
action_ref TEXT NOT NULL,
|
||||
action_digest TEXT NOT NULL,
|
||||
preview_digest TEXT NOT NULL,
|
||||
redaction_contract_digest TEXT NOT NULL,
|
||||
artifact_digest TEXT NOT NULL,
|
||||
byte_length INTEGER NOT NULL,
|
||||
sealed_at_ms INTEGER NOT NULL,
|
||||
artifact_json TEXT NOT NULL,
|
||||
CONSTRAINT ql3_tool_preview_artifact_project_fk
|
||||
FOREIGN KEY (project_id)
|
||||
REFERENCES "QingLong3Projects" (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_preview_artifact_identity_check CHECK (
|
||||
length(artifact_id) BETWEEN 1 AND 128 AND
|
||||
length(project_id) BETWEEN 1 AND 128 AND
|
||||
length(action_ref) BETWEEN 1 AND 255
|
||||
),
|
||||
CONSTRAINT ql3_tool_preview_artifact_digest_check CHECK (
|
||||
length(action_digest) = 64 AND
|
||||
action_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(preview_digest) = 64 AND
|
||||
preview_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(redaction_contract_digest) = 64 AND
|
||||
redaction_contract_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(artifact_digest) = 64 AND
|
||||
artifact_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
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 (
|
||||
length(CAST(artifact_json AS BLOB)) BETWEEN 2 AND 16384 AND
|
||||
json_valid(artifact_json) AND json_type(artifact_json) = 'object' AND
|
||||
json_extract(artifact_json, '$.schema') =
|
||||
'qinglong/tool-invocation-preview-artifact@v1' AND
|
||||
json_extract(artifact_json, '$.artifactId') = artifact_id AND
|
||||
json_extract(artifact_json, '$.projectId') = project_id AND
|
||||
json_extract(artifact_json, '$.actionRef') = action_ref AND
|
||||
json_extract(artifact_json, '$.actionDigest') = action_digest AND
|
||||
json_extract(artifact_json, '$.previewDigest') = preview_digest AND
|
||||
json_extract(artifact_json, '$.redactionContractDigest') =
|
||||
redaction_contract_digest AND
|
||||
json_extract(artifact_json, '$.artifactDigest') = artifact_digest AND
|
||||
json_extract(artifact_json, '$.byteLength') IS byte_length AND
|
||||
json_extract(artifact_json, '$.sealedAtMs') IS sealed_at_ms
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_tool_preview_artifact_action_uidx ON "ToolInvocationPreviewArtifacts" (project_id, action_ref)`,
|
||||
`CREATE UNIQUE INDEX ql3_tool_preview_artifact_action_digest_uidx ON "ToolInvocationPreviewArtifacts" (action_digest)`,
|
||||
`CREATE INDEX ql3_tool_preview_artifact_project_time_idx ON "ToolInvocationPreviewArtifacts" (project_id, sealed_at_ms, artifact_id)`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
const PREVIOUS_CAPABILITIES =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1}';
|
||||
const NEXT_CAPABILITIES =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1}';
|
||||
|
||||
export const local0058CapabilityV29Migration = defineLocalSqliteMigration({
|
||||
id: '0058-capability-v29',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 29,
|
||||
migration_id = '0057-tool-invocation-artifacts',
|
||||
capabilities = '${NEXT_CAPABILITIES}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 28
|
||||
AND migration_id = '0055-tool-execution-start-barriers'
|
||||
AND capabilities = '${PREVIOUS_CAPABILITIES}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0059ToolExecutionArtifactBindingsMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0059-tool-execution-artifact-bindings',
|
||||
statements: [
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_tool_input_artifact_start_binding_uidx
|
||||
ON "ToolInvocationInputArtifacts" (
|
||||
artifact_id, artifact_digest, project_id, action_ref, input_digest
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_tool_preview_artifact_start_binding_uidx
|
||||
ON "ToolInvocationPreviewArtifacts" (
|
||||
artifact_id, artifact_digest, project_id, action_ref, action_digest,
|
||||
preview_digest, redaction_contract_digest
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE TABLE "ToolExecutionStartArtifactBindings" (
|
||||
start_id TEXT PRIMARY KEY NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
action_ref TEXT NOT NULL,
|
||||
input_artifact_id TEXT NOT NULL,
|
||||
input_artifact_digest TEXT NOT NULL,
|
||||
input_digest TEXT NOT NULL,
|
||||
preview_artifact_id TEXT NOT NULL,
|
||||
preview_artifact_digest TEXT NOT NULL,
|
||||
action_digest TEXT NOT NULL,
|
||||
preview_digest TEXT NOT NULL,
|
||||
redaction_contract_digest TEXT NOT NULL,
|
||||
bound_at_ms INTEGER NOT NULL,
|
||||
CONSTRAINT ql3_tool_start_artifact_barrier_fk
|
||||
FOREIGN KEY (start_id)
|
||||
REFERENCES "ToolExecutionStartBarriers" (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 "ToolInvocationInputArtifacts" (
|
||||
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 "ToolInvocationPreviewArtifacts" (
|
||||
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 (
|
||||
length(start_id) BETWEEN 1 AND 128 AND
|
||||
length(project_id) BETWEEN 1 AND 128 AND
|
||||
length(action_ref) BETWEEN 1 AND 255 AND
|
||||
length(input_artifact_id) BETWEEN 1 AND 128 AND
|
||||
length(preview_artifact_id) BETWEEN 1 AND 128
|
||||
),
|
||||
CONSTRAINT ql3_tool_start_artifact_digest_check CHECK (
|
||||
length(input_artifact_digest) = 64 AND
|
||||
input_artifact_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(input_digest) = 64 AND
|
||||
input_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(preview_artifact_digest) = 64 AND
|
||||
preview_artifact_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(action_digest) = 64 AND
|
||||
action_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(preview_digest) = 64 AND
|
||||
preview_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(redaction_contract_digest) = 64 AND
|
||||
redaction_contract_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_tool_start_artifact_time_check CHECK (bound_at_ms >= 0)
|
||||
)
|
||||
`,
|
||||
`CREATE INDEX ql3_tool_start_artifact_input_idx ON "ToolExecutionStartArtifactBindings" (input_artifact_id, start_id)`,
|
||||
`CREATE INDEX ql3_tool_start_artifact_preview_idx ON "ToolExecutionStartArtifactBindings" (preview_artifact_id, start_id)`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V29 =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1}';
|
||||
const CAPABILITIES_V30 =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1}';
|
||||
|
||||
export const local0060CapabilityV30Migration = defineLocalSqliteMigration({
|
||||
id: '0060-capability-v30',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 30,
|
||||
migration_id = '0059-tool-execution-artifact-bindings',
|
||||
capabilities = '${CAPABILITIES_V30}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 29
|
||||
AND migration_id = '0057-tool-invocation-artifacts'
|
||||
AND capabilities = '${CAPABILITIES_V29}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0061ToolExecutionCompletionsMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0061-tool-execution-completions',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ToolExecutionCompletions" (
|
||||
start_id TEXT PRIMARY KEY NOT NULL,
|
||||
artifact_id TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
run_id TEXT NOT NULL,
|
||||
step_run_id TEXT NOT NULL,
|
||||
started_step_run_version INTEGER NOT NULL,
|
||||
completed_step_run_version INTEGER NOT NULL,
|
||||
barrier_digest TEXT NOT NULL,
|
||||
adapter_digest TEXT NOT NULL,
|
||||
output_digest TEXT NOT NULL,
|
||||
execution_result_digest TEXT NOT NULL,
|
||||
artifact_digest TEXT NOT NULL,
|
||||
key_id TEXT NOT NULL,
|
||||
algorithm TEXT NOT NULL,
|
||||
plaintext_bytes INTEGER NOT NULL,
|
||||
step_run_mutation_id TEXT NOT NULL,
|
||||
step_run_mutation_digest TEXT NOT NULL,
|
||||
completed_step_run_digest TEXT NOT NULL,
|
||||
run_event_id TEXT NOT NULL,
|
||||
completed_at_ms INTEGER NOT NULL,
|
||||
completion_digest TEXT NOT NULL,
|
||||
artifact_json TEXT NOT NULL,
|
||||
completion_json TEXT NOT NULL,
|
||||
CONSTRAINT ql3_tool_completion_start_fk
|
||||
FOREIGN KEY (start_id)
|
||||
REFERENCES "ToolExecutionStartBarriers" (start_id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_completion_step_fk
|
||||
FOREIGN KEY (run_id, step_run_id)
|
||||
REFERENCES "StepRuns" (run_id, id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_completion_mutation_fk
|
||||
FOREIGN KEY (step_run_mutation_id)
|
||||
REFERENCES "StepRunMutations" (mutation_id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_completion_event_fk
|
||||
FOREIGN KEY (run_event_id)
|
||||
REFERENCES "RunEvents" (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_completion_identity_check CHECK (
|
||||
length(start_id) BETWEEN 1 AND 128 AND
|
||||
length(artifact_id) BETWEEN 1 AND 128 AND
|
||||
length(project_id) BETWEEN 1 AND 128 AND
|
||||
length(run_id) BETWEEN 1 AND 128 AND
|
||||
length(step_run_id) BETWEEN 1 AND 128 AND
|
||||
length(key_id) BETWEEN 1 AND 128 AND
|
||||
length(step_run_mutation_id) BETWEEN 1 AND 128 AND
|
||||
length(run_event_id) BETWEEN 1 AND 128
|
||||
),
|
||||
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 (
|
||||
length(barrier_digest) = 64 AND
|
||||
barrier_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(adapter_digest) = 64 AND
|
||||
adapter_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(output_digest) = 64 AND
|
||||
output_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(execution_result_digest) = 64 AND
|
||||
execution_result_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(artifact_digest) = 64 AND
|
||||
artifact_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(step_run_mutation_digest) = 64 AND
|
||||
step_run_mutation_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(completed_step_run_digest) = 64 AND
|
||||
completed_step_run_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(completion_digest) = 64 AND
|
||||
completion_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_tool_completion_budget_check CHECK (
|
||||
algorithm = 'aes-256-gcm' AND
|
||||
plaintext_bytes BETWEEN 0 AND 262144 AND
|
||||
completed_at_ms >= 0 AND
|
||||
length(CAST(artifact_json AS BLOB)) BETWEEN 2 AND 393216 AND
|
||||
length(CAST(completion_json AS BLOB)) BETWEEN 2 AND 24576
|
||||
),
|
||||
CONSTRAINT ql3_tool_completion_json_check CHECK (
|
||||
json_valid(artifact_json) AND
|
||||
json_type(artifact_json) = 'object' AND
|
||||
json_extract(artifact_json, '$.schema') =
|
||||
'qinglong/tool-execution-result-artifact@v1' AND
|
||||
json_extract(artifact_json, '$.artifactId') = artifact_id AND
|
||||
json_extract(artifact_json, '$.projectId') = project_id AND
|
||||
json_extract(artifact_json, '$.startId') = start_id AND
|
||||
json_extract(artifact_json, '$.runId') = run_id AND
|
||||
json_extract(artifact_json, '$.stepRunId') = step_run_id AND
|
||||
json_extract(artifact_json, '$.barrierDigest') = barrier_digest AND
|
||||
json_extract(artifact_json, '$.adapterDigest') = adapter_digest AND
|
||||
json_extract(artifact_json, '$.outputDigest') = output_digest AND
|
||||
json_extract(artifact_json, '$.executionResultDigest') =
|
||||
execution_result_digest AND
|
||||
json_extract(artifact_json, '$.artifactDigest') = artifact_digest AND
|
||||
json_extract(artifact_json, '$.keyId') = key_id AND
|
||||
json_extract(artifact_json, '$.algorithm') = algorithm AND
|
||||
json_extract(artifact_json, '$.plaintextBytes') = plaintext_bytes AND
|
||||
json_extract(artifact_json, '$.sealedAtMs') = completed_at_ms AND
|
||||
json_valid(completion_json) AND
|
||||
json_type(completion_json) = 'object' AND
|
||||
json_extract(completion_json, '$.schema') =
|
||||
'qinglong/tool-execution-completion@v1' AND
|
||||
json_extract(completion_json, '$.startId') = start_id AND
|
||||
json_extract(completion_json, '$.projectId') = project_id AND
|
||||
json_extract(completion_json, '$.runId') = run_id AND
|
||||
json_extract(completion_json, '$.stepRunId') = step_run_id AND
|
||||
json_extract(completion_json, '$.startedStepRunVersion') =
|
||||
started_step_run_version AND
|
||||
json_extract(completion_json, '$.completedStepRunVersion') =
|
||||
completed_step_run_version AND
|
||||
json_extract(completion_json, '$.barrierDigest') = barrier_digest AND
|
||||
json_extract(completion_json, '$.adapterDigest') = adapter_digest AND
|
||||
json_extract(completion_json, '$.resultArtifact.artifactId') =
|
||||
artifact_id AND
|
||||
json_extract(completion_json, '$.resultArtifact.artifactDigest') =
|
||||
artifact_digest AND
|
||||
json_extract(completion_json, '$.resultArtifact.outputDigest') =
|
||||
output_digest AND
|
||||
json_extract(
|
||||
completion_json, '$.resultArtifact.executionResultDigest'
|
||||
) = execution_result_digest AND
|
||||
json_extract(completion_json, '$.stepRunMutationId') =
|
||||
step_run_mutation_id AND
|
||||
json_extract(completion_json, '$.stepRunMutationDigest') =
|
||||
step_run_mutation_digest AND
|
||||
json_extract(completion_json, '$.completedStepRunDigest') =
|
||||
completed_step_run_digest AND
|
||||
json_extract(completion_json, '$.runEventId') = run_event_id AND
|
||||
json_extract(completion_json, '$.completedAtMs') = completed_at_ms AND
|
||||
json_extract(completion_json, '$.completionDigest') = completion_digest
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_tool_completion_artifact_uidx ON "ToolExecutionCompletions" (artifact_id)`,
|
||||
`CREATE UNIQUE INDEX ql3_tool_completion_mutation_uidx ON "ToolExecutionCompletions" (step_run_mutation_id)`,
|
||||
`CREATE UNIQUE INDEX ql3_tool_completion_event_uidx ON "ToolExecutionCompletions" (run_event_id)`,
|
||||
`CREATE UNIQUE INDEX ql3_tool_completion_step_version_uidx ON "ToolExecutionCompletions" (run_id, step_run_id, completed_step_run_version)`,
|
||||
`CREATE INDEX ql3_tool_completion_project_time_idx ON "ToolExecutionCompletions" (project_id, completed_at_ms, start_id)`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V30 =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1}';
|
||||
const CAPABILITIES_V31 =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1}';
|
||||
|
||||
export const local0062CapabilityV31Migration = defineLocalSqliteMigration({
|
||||
id: '0062-capability-v31',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 31,
|
||||
migration_id = '0061-tool-execution-completions',
|
||||
capabilities = '${CAPABILITIES_V31}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 30
|
||||
AND migration_id = '0059-tool-execution-artifact-bindings'
|
||||
AND capabilities = '${CAPABILITIES_V30}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0063ToolExecutionFailureCompletionsMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0063-tool-execution-failure-completions',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ToolExecutionFailureCompletions" (
|
||||
start_id TEXT PRIMARY KEY NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
run_id TEXT NOT NULL,
|
||||
step_run_id TEXT NOT NULL,
|
||||
started_step_run_version INTEGER NOT NULL,
|
||||
completed_step_run_version INTEGER NOT NULL,
|
||||
barrier_digest TEXT NOT NULL,
|
||||
adapter_digest TEXT NOT NULL,
|
||||
outcome TEXT NOT NULL,
|
||||
result_code TEXT NOT NULL,
|
||||
error_summary TEXT NOT NULL,
|
||||
step_run_mutation_id TEXT NOT NULL,
|
||||
step_run_mutation_digest TEXT NOT NULL,
|
||||
completed_step_run_digest TEXT NOT NULL,
|
||||
run_event_id TEXT NOT NULL,
|
||||
completed_at_ms INTEGER NOT NULL,
|
||||
completion_digest TEXT NOT NULL,
|
||||
completion_json TEXT NOT NULL,
|
||||
CONSTRAINT ql3_tool_failure_completion_start_fk
|
||||
FOREIGN KEY (start_id)
|
||||
REFERENCES "ToolExecutionStartBarriers" (start_id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_failure_completion_step_fk
|
||||
FOREIGN KEY (run_id, step_run_id)
|
||||
REFERENCES "StepRuns" (run_id, id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_failure_completion_mutation_fk
|
||||
FOREIGN KEY (step_run_mutation_id)
|
||||
REFERENCES "StepRunMutations" (mutation_id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_failure_completion_event_fk
|
||||
FOREIGN KEY (run_event_id)
|
||||
REFERENCES "RunEvents" (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_failure_completion_identity_check CHECK (
|
||||
length(start_id) BETWEEN 1 AND 128 AND
|
||||
length(project_id) BETWEEN 1 AND 128 AND
|
||||
length(run_id) BETWEEN 1 AND 128 AND
|
||||
length(step_run_id) BETWEEN 1 AND 128 AND
|
||||
length(step_run_mutation_id) BETWEEN 1 AND 128 AND
|
||||
length(run_event_id) BETWEEN 1 AND 128
|
||||
),
|
||||
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 (
|
||||
length(barrier_digest) = 64 AND
|
||||
barrier_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(adapter_digest) = 64 AND
|
||||
adapter_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(step_run_mutation_digest) = 64 AND
|
||||
step_run_mutation_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(completed_step_run_digest) = 64 AND
|
||||
completed_step_run_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(completion_digest) = 64 AND
|
||||
completion_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
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
|
||||
length(CAST(completion_json AS BLOB)) BETWEEN 2 AND 24576
|
||||
),
|
||||
CONSTRAINT ql3_tool_failure_completion_json_check CHECK (
|
||||
json_valid(completion_json) AND
|
||||
json_type(completion_json) = 'object' AND
|
||||
json_extract(completion_json, '$.schema') =
|
||||
'qinglong/tool-execution-failure-completion@v1' AND
|
||||
json_extract(completion_json, '$.startId') = start_id AND
|
||||
json_extract(completion_json, '$.projectId') = project_id AND
|
||||
json_extract(completion_json, '$.runId') = run_id AND
|
||||
json_extract(completion_json, '$.stepRunId') = step_run_id AND
|
||||
json_extract(completion_json, '$.startedStepRunVersion') =
|
||||
started_step_run_version AND
|
||||
json_extract(completion_json, '$.completedStepRunVersion') =
|
||||
completed_step_run_version AND
|
||||
json_extract(completion_json, '$.barrierDigest') = barrier_digest AND
|
||||
json_extract(completion_json, '$.adapterDigest') = adapter_digest AND
|
||||
json_extract(completion_json, '$.outcome') = outcome AND
|
||||
json_extract(completion_json, '$.resultCode') = result_code AND
|
||||
json_extract(completion_json, '$.errorSummary') = error_summary AND
|
||||
json_extract(completion_json, '$.stepRunMutationId') =
|
||||
step_run_mutation_id AND
|
||||
json_extract(completion_json, '$.stepRunMutationDigest') =
|
||||
step_run_mutation_digest AND
|
||||
json_extract(completion_json, '$.completedStepRunDigest') =
|
||||
completed_step_run_digest AND
|
||||
json_extract(completion_json, '$.runEventId') = run_event_id AND
|
||||
json_extract(completion_json, '$.completedAtMs') = completed_at_ms AND
|
||||
json_extract(completion_json, '$.completionDigest') = completion_digest
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_tool_failure_completion_mutation_uidx ON "ToolExecutionFailureCompletions" (step_run_mutation_id)`,
|
||||
`CREATE UNIQUE INDEX ql3_tool_failure_completion_event_uidx ON "ToolExecutionFailureCompletions" (run_event_id)`,
|
||||
`CREATE UNIQUE INDEX ql3_tool_failure_completion_step_version_uidx ON "ToolExecutionFailureCompletions" (run_id, step_run_id, completed_step_run_version)`,
|
||||
`CREATE INDEX ql3_tool_failure_completion_project_time_idx ON "ToolExecutionFailureCompletions" (project_id, completed_at_ms, start_id)`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V31 =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1}';
|
||||
const CAPABILITIES_V32 =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1}';
|
||||
|
||||
export const local0064CapabilityV32Migration = defineLocalSqliteMigration({
|
||||
id: '0064-capability-v32',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 32,
|
||||
migration_id = '0063-tool-execution-failure-completions',
|
||||
capabilities = '${CAPABILITIES_V32}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 31
|
||||
AND migration_id = '0061-tool-execution-completions'
|
||||
AND capabilities = '${CAPABILITIES_V31}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,155 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0065ToolResultKeyCatalogMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0065-tool-result-key-catalog',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ToolResultKeyCatalogGenerations" (
|
||||
authority TEXT NOT NULL,
|
||||
generation INTEGER NOT NULL,
|
||||
previous_generation INTEGER,
|
||||
previous_catalog_digest TEXT,
|
||||
active_key_id TEXT,
|
||||
mutation_kind TEXT NOT NULL,
|
||||
mutation_id TEXT NOT NULL,
|
||||
catalog_digest TEXT NOT NULL,
|
||||
command_digest TEXT NOT NULL,
|
||||
committed_at_ms INTEGER NOT NULL,
|
||||
catalog_json TEXT NOT NULL,
|
||||
PRIMARY KEY (authority, generation),
|
||||
UNIQUE (authority, generation, catalog_digest),
|
||||
UNIQUE (mutation_id),
|
||||
UNIQUE (catalog_digest),
|
||||
CONSTRAINT ql3_tool_result_key_catalog_previous_fk
|
||||
FOREIGN KEY (
|
||||
authority, previous_generation, previous_catalog_digest
|
||||
)
|
||||
REFERENCES "ToolResultKeyCatalogGenerations" (
|
||||
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
|
||||
length(active_key_id) BETWEEN 1 AND 128 AND
|
||||
active_key_id NOT GLOB '*[^A-Za-z0-9._-]*' AND
|
||||
substr(active_key_id, 1, 1) GLOB '[A-Za-z0-9]'
|
||||
),
|
||||
CONSTRAINT ql3_tool_result_key_catalog_digest_check CHECK (
|
||||
(
|
||||
previous_catalog_digest IS NULL OR
|
||||
length(previous_catalog_digest) = 64 AND
|
||||
previous_catalog_digest NOT GLOB '*[^0-9a-f]*'
|
||||
) AND
|
||||
length(catalog_digest) = 64 AND
|
||||
catalog_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(command_digest) = 64 AND
|
||||
command_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_tool_result_key_catalog_budget_check CHECK (
|
||||
committed_at_ms >= 0 AND
|
||||
length(mutation_id) BETWEEN 1 AND 128 AND
|
||||
length(CAST(catalog_json AS BLOB)) BETWEEN 2 AND 65536
|
||||
),
|
||||
CONSTRAINT ql3_tool_result_key_catalog_json_check CHECK (
|
||||
json_valid(catalog_json) AND
|
||||
json_type(catalog_json) = 'object' AND
|
||||
json_extract(catalog_json, '$.schema') =
|
||||
'qinglong/tool-result-key-catalog@v1' AND
|
||||
json_extract(catalog_json, '$.generation') = generation AND
|
||||
(
|
||||
(
|
||||
previous_catalog_digest IS NULL AND
|
||||
json_type(catalog_json, '$.previousCatalogDigest') = 'null'
|
||||
) OR
|
||||
json_extract(catalog_json, '$.previousCatalogDigest') =
|
||||
previous_catalog_digest
|
||||
) AND
|
||||
(
|
||||
(
|
||||
active_key_id IS NULL AND
|
||||
json_type(catalog_json, '$.activeKeyId') = 'null'
|
||||
) OR
|
||||
json_extract(catalog_json, '$.activeKeyId') = active_key_id
|
||||
) AND
|
||||
json_extract(catalog_json, '$.mutationKind') = mutation_kind AND
|
||||
json_extract(catalog_json, '$.mutationId') = mutation_id AND
|
||||
json_extract(catalog_json, '$.catalogDigest') = catalog_digest AND
|
||||
json_extract(catalog_json, '$.committedAtMs') = committed_at_ms AND
|
||||
json_type(catalog_json, '$.keys') = 'array' AND
|
||||
json_array_length(json_extract(catalog_json, '$.keys'))
|
||||
BETWEEN 1 AND 64
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE INDEX ql3_tool_result_key_catalog_current_idx ON "ToolResultKeyCatalogGenerations" (authority, generation DESC)`,
|
||||
`
|
||||
CREATE TABLE "ToolExecutionResultKeyBindings" (
|
||||
start_id TEXT PRIMARY KEY NOT NULL,
|
||||
artifact_id TEXT NOT NULL UNIQUE,
|
||||
artifact_digest TEXT NOT NULL,
|
||||
catalog_authority TEXT NOT NULL,
|
||||
catalog_generation INTEGER NOT NULL,
|
||||
catalog_digest TEXT NOT NULL,
|
||||
key_id TEXT NOT NULL,
|
||||
material_proof TEXT NOT NULL,
|
||||
binding_digest TEXT NOT NULL UNIQUE,
|
||||
CONSTRAINT ql3_tool_result_key_binding_completion_fk
|
||||
FOREIGN KEY (start_id)
|
||||
REFERENCES "ToolExecutionCompletions" (start_id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_result_key_binding_artifact_fk
|
||||
FOREIGN KEY (artifact_id)
|
||||
REFERENCES "ToolExecutionCompletions" (artifact_id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_result_key_binding_catalog_fk
|
||||
FOREIGN KEY (
|
||||
catalog_authority, catalog_generation, catalog_digest
|
||||
)
|
||||
REFERENCES "ToolResultKeyCatalogGenerations" (
|
||||
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 (
|
||||
length(start_id) BETWEEN 1 AND 128 AND
|
||||
length(artifact_id) BETWEEN 1 AND 128 AND
|
||||
length(key_id) BETWEEN 1 AND 128 AND
|
||||
key_id NOT GLOB '*[^A-Za-z0-9._-]*' AND
|
||||
substr(key_id, 1, 1) GLOB '[A-Za-z0-9]' AND
|
||||
catalog_generation BETWEEN 1 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_tool_result_key_binding_digest_check CHECK (
|
||||
length(artifact_digest) = 64 AND
|
||||
artifact_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(catalog_digest) = 64 AND
|
||||
catalog_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(material_proof) = 64 AND
|
||||
material_proof NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(binding_digest) = 64 AND
|
||||
binding_digest NOT GLOB '*[^0-9a-f]*'
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE INDEX ql3_tool_result_key_binding_catalog_idx ON "ToolExecutionResultKeyBindings" (catalog_generation, key_id, start_id)`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V32 =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1}';
|
||||
const CAPABILITIES_V33 =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1}';
|
||||
|
||||
export const local0066CapabilityV33Migration = defineLocalSqliteMigration({
|
||||
id: '0066-capability-v33',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 33,
|
||||
migration_id = '0065-tool-result-key-catalog',
|
||||
capabilities = '${CAPABILITIES_V33}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 32
|
||||
AND migration_id = '0063-tool-execution-failure-completions'
|
||||
AND capabilities = '${CAPABILITIES_V32}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,238 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0067ToolResultRekeyOverlaysMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0067-tool-result-rekey-overlays',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ToolExecutionResultRekeyOverlays" (
|
||||
overlay_id TEXT PRIMARY KEY NOT NULL,
|
||||
artifact_id TEXT NOT NULL,
|
||||
source_binding_digest TEXT NOT NULL,
|
||||
revision INTEGER NOT NULL,
|
||||
previous_overlay_digest TEXT,
|
||||
from_key_id TEXT NOT NULL,
|
||||
target_catalog_authority TEXT NOT NULL,
|
||||
target_catalog_generation INTEGER NOT NULL,
|
||||
target_catalog_digest TEXT NOT NULL,
|
||||
target_key_id TEXT NOT NULL,
|
||||
target_material_proof TEXT NOT NULL,
|
||||
mutation_id TEXT NOT NULL UNIQUE,
|
||||
command_digest TEXT NOT NULL,
|
||||
overlay_digest TEXT NOT NULL UNIQUE,
|
||||
rekeyed_at_ms INTEGER NOT NULL,
|
||||
overlay_json TEXT NOT NULL,
|
||||
UNIQUE (artifact_id, revision),
|
||||
UNIQUE (artifact_id, revision, overlay_digest),
|
||||
CONSTRAINT ql3_tool_result_rekey_artifact_fk
|
||||
FOREIGN KEY (artifact_id)
|
||||
REFERENCES "ToolExecutionResultKeyBindings" (artifact_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_result_rekey_binding_fk
|
||||
FOREIGN KEY (source_binding_digest)
|
||||
REFERENCES "ToolExecutionResultKeyBindings" (binding_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_result_rekey_previous_fk
|
||||
FOREIGN KEY (previous_overlay_digest)
|
||||
REFERENCES "ToolExecutionResultRekeyOverlays" (overlay_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_result_rekey_catalog_fk
|
||||
FOREIGN KEY (
|
||||
target_catalog_authority,
|
||||
target_catalog_generation,
|
||||
target_catalog_digest
|
||||
)
|
||||
REFERENCES "ToolResultKeyCatalogGenerations" (
|
||||
authority, generation, catalog_digest
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_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_tool_result_rekey_authority_check CHECK (
|
||||
target_catalog_authority = 'trusted-tool-results'
|
||||
),
|
||||
CONSTRAINT ql3_tool_result_rekey_identity_check CHECK (
|
||||
length(overlay_id) BETWEEN 1 AND 128 AND
|
||||
length(artifact_id) BETWEEN 1 AND 128 AND
|
||||
length(mutation_id) BETWEEN 1 AND 128 AND
|
||||
length(from_key_id) BETWEEN 1 AND 128 AND
|
||||
from_key_id NOT GLOB '*[^A-Za-z0-9._-]*' AND
|
||||
substr(from_key_id, 1, 1) GLOB '[A-Za-z0-9]' AND
|
||||
length(target_key_id) BETWEEN 1 AND 128 AND
|
||||
target_key_id NOT GLOB '*[^A-Za-z0-9._-]*' AND
|
||||
substr(target_key_id, 1, 1) GLOB '[A-Za-z0-9]' AND
|
||||
from_key_id <> target_key_id AND
|
||||
target_catalog_generation BETWEEN 1 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_tool_result_rekey_digest_check CHECK (
|
||||
(
|
||||
previous_overlay_digest IS NULL OR
|
||||
length(previous_overlay_digest) = 64 AND
|
||||
previous_overlay_digest NOT GLOB '*[^0-9a-f]*'
|
||||
) AND
|
||||
length(source_binding_digest) = 64 AND
|
||||
source_binding_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(target_catalog_digest) = 64 AND
|
||||
target_catalog_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(target_material_proof) = 64 AND
|
||||
target_material_proof NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(command_digest) = 64 AND
|
||||
command_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(overlay_digest) = 64 AND
|
||||
overlay_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_tool_result_rekey_budget_check CHECK (
|
||||
rekeyed_at_ms >= 0 AND
|
||||
length(CAST(overlay_json AS BLOB)) BETWEEN 2 AND 393216
|
||||
),
|
||||
CONSTRAINT ql3_tool_result_rekey_json_check CHECK (
|
||||
json_valid(overlay_json) AND
|
||||
json_type(overlay_json) = 'object' AND
|
||||
json_extract(overlay_json, '$.schema') =
|
||||
'qinglong/tool-execution-result-rekey-overlay@v1' AND
|
||||
json_extract(overlay_json, '$.overlayId') = overlay_id AND
|
||||
json_extract(overlay_json, '$.sourceArtifact.artifactId') =
|
||||
artifact_id AND
|
||||
json_extract(overlay_json, '$.sourceBindingDigest') =
|
||||
source_binding_digest AND
|
||||
json_extract(overlay_json, '$.revision') = revision AND
|
||||
(
|
||||
(
|
||||
previous_overlay_digest IS NULL AND
|
||||
json_type(overlay_json, '$.previousOverlayDigest') = 'null'
|
||||
) OR
|
||||
json_extract(overlay_json, '$.previousOverlayDigest') =
|
||||
previous_overlay_digest
|
||||
) AND
|
||||
json_extract(overlay_json, '$.fromKeyId') = from_key_id AND
|
||||
json_extract(overlay_json, '$.targetCatalogFence.generation') =
|
||||
target_catalog_generation AND
|
||||
json_extract(overlay_json, '$.targetCatalogFence.catalogDigest') =
|
||||
target_catalog_digest AND
|
||||
json_extract(overlay_json, '$.targetCatalogFence.keyId') =
|
||||
target_key_id AND
|
||||
json_extract(overlay_json, '$.targetCatalogFence.materialProof') =
|
||||
target_material_proof AND
|
||||
json_extract(overlay_json, '$.rekeyedAtMs') = rekeyed_at_ms AND
|
||||
json_extract(overlay_json, '$.overlayDigest') = overlay_digest
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE INDEX ql3_tool_result_rekey_artifact_idx ON "ToolExecutionResultRekeyOverlays" (artifact_id, revision DESC)`,
|
||||
`CREATE INDEX ql3_tool_result_rekey_target_idx ON "ToolExecutionResultRekeyOverlays" (target_key_id, artifact_id, revision DESC)`,
|
||||
`
|
||||
CREATE TABLE "ToolExecutionResultRekeyHeads" (
|
||||
artifact_id TEXT PRIMARY KEY NOT NULL,
|
||||
revision INTEGER NOT NULL,
|
||||
overlay_id TEXT NOT NULL UNIQUE,
|
||||
overlay_digest TEXT NOT NULL UNIQUE,
|
||||
target_catalog_generation INTEGER NOT NULL,
|
||||
target_catalog_digest TEXT NOT NULL,
|
||||
target_key_id TEXT NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
CONSTRAINT ql3_tool_result_rekey_head_overlay_fk
|
||||
FOREIGN KEY (artifact_id, revision, overlay_digest)
|
||||
REFERENCES "ToolExecutionResultRekeyOverlays" (
|
||||
artifact_id, revision, overlay_digest
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_result_rekey_head_identity_check CHECK (
|
||||
revision BETWEEN 1 AND 2147483647 AND
|
||||
length(artifact_id) BETWEEN 1 AND 128 AND
|
||||
length(overlay_id) BETWEEN 1 AND 128 AND
|
||||
length(target_key_id) BETWEEN 1 AND 128 AND
|
||||
target_key_id NOT GLOB '*[^A-Za-z0-9._-]*' AND
|
||||
substr(target_key_id, 1, 1) GLOB '[A-Za-z0-9]' AND
|
||||
target_catalog_generation BETWEEN 1 AND 2147483647 AND
|
||||
updated_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_tool_result_rekey_head_digest_check CHECK (
|
||||
length(overlay_digest) = 64 AND
|
||||
overlay_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(target_catalog_digest) = 64 AND
|
||||
target_catalog_digest NOT GLOB '*[^0-9a-f]*'
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE INDEX ql3_tool_result_rekey_head_target_idx ON "ToolExecutionResultRekeyHeads" (target_key_id, artifact_id)`,
|
||||
`
|
||||
CREATE TABLE "ToolResultKeyRetirementReceipts" (
|
||||
receipt_digest TEXT PRIMARY KEY NOT NULL,
|
||||
catalog_authority TEXT NOT NULL,
|
||||
catalog_generation INTEGER NOT NULL,
|
||||
catalog_digest TEXT NOT NULL,
|
||||
key_id TEXT NOT NULL,
|
||||
material_proof TEXT NOT NULL,
|
||||
mutation_id TEXT NOT NULL UNIQUE,
|
||||
command_digest TEXT 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 TEXT NOT NULL,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
receipt_json TEXT NOT NULL,
|
||||
CONSTRAINT ql3_tool_result_key_retirement_catalog_fk
|
||||
FOREIGN KEY (
|
||||
catalog_authority, catalog_generation, catalog_digest
|
||||
)
|
||||
REFERENCES "ToolResultKeyCatalogGenerations" (
|
||||
authority, generation, catalog_digest
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_result_key_retirement_authority_check CHECK (
|
||||
catalog_authority = 'trusted-tool-results'
|
||||
),
|
||||
CONSTRAINT ql3_tool_result_key_retirement_identity_check CHECK (
|
||||
catalog_generation BETWEEN 1 AND 2147483647 AND
|
||||
length(key_id) BETWEEN 1 AND 128 AND
|
||||
key_id NOT GLOB '*[^A-Za-z0-9._-]*' AND
|
||||
substr(key_id, 1, 1) GLOB '[A-Za-z0-9]' AND
|
||||
length(mutation_id) BETWEEN 1 AND 128
|
||||
),
|
||||
CONSTRAINT ql3_tool_result_key_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_tool_result_key_retirement_digest_check CHECK (
|
||||
length(receipt_digest) = 64 AND
|
||||
receipt_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(catalog_digest) = 64 AND
|
||||
catalog_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(material_proof) = 64 AND
|
||||
material_proof NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(command_digest) = 64 AND
|
||||
command_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(coverage_digest) = 64 AND
|
||||
coverage_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_tool_result_key_retirement_json_check CHECK (
|
||||
json_valid(receipt_json) AND
|
||||
json_type(receipt_json) = 'object' AND
|
||||
json_extract(receipt_json, '$.schema') =
|
||||
'qinglong/tool-result-key-retirement-receipt@v1' AND
|
||||
json_extract(receipt_json, '$.catalogGeneration') =
|
||||
catalog_generation AND
|
||||
json_extract(receipt_json, '$.catalogDigest') = catalog_digest AND
|
||||
json_extract(receipt_json, '$.keyId') = key_id AND
|
||||
json_extract(receipt_json, '$.materialProof') = material_proof AND
|
||||
json_extract(receipt_json, '$.mutationId') = mutation_id AND
|
||||
json_extract(receipt_json, '$.bindingCount') = binding_count AND
|
||||
json_extract(receipt_json, '$.overlayHeadCount') =
|
||||
overlay_head_count AND
|
||||
json_extract(receipt_json, '$.uncoveredBindingCount') = 0 AND
|
||||
json_extract(receipt_json, '$.uncoveredOverlayHeadCount') = 0 AND
|
||||
json_extract(receipt_json, '$.coverageDigest') = coverage_digest AND
|
||||
json_extract(receipt_json, '$.createdAtMs') = created_at_ms AND
|
||||
json_extract(receipt_json, '$.receiptDigest') = receipt_digest
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE INDEX ql3_tool_result_key_retirement_catalog_idx ON "ToolResultKeyRetirementReceipts" (catalog_generation, key_id)`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V33 =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1}';
|
||||
const CAPABILITIES_V34 =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1}';
|
||||
|
||||
export const local0068CapabilityV34Migration = defineLocalSqliteMigration({
|
||||
id: '0068-capability-v34',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 34,
|
||||
migration_id = '0067-tool-result-rekey-overlays',
|
||||
capabilities = '${CAPABILITIES_V34}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 33
|
||||
AND migration_id = '0065-tool-result-key-catalog'
|
||||
AND capabilities = '${CAPABILITIES_V33}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,298 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0069PluginPackageQuarantineMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0069-plugin-package-quarantine',
|
||||
statements: [
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_plugin_package_installs_quarantine_target_uidx
|
||||
ON "QingLong3PluginPackageInstalls" (
|
||||
project_id, package_name, installation_id, lock_digest, record_digest
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_project_tool_definition_snapshot_withdrawal_uidx
|
||||
ON "QingLong3ProjectToolDefinitionSnapshots" (
|
||||
project_id, active_vector_digest, snapshot_digest
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE TABLE "QingLong3PluginPackageQuarantineEvents" (
|
||||
event_digest TEXT PRIMARY KEY NOT NULL,
|
||||
mutation_id TEXT NOT NULL,
|
||||
revocation_receipt_digest TEXT NOT NULL,
|
||||
impact_digest TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
package_name TEXT NOT NULL,
|
||||
installation_id TEXT NOT NULL,
|
||||
lock_digest TEXT NOT NULL,
|
||||
install_state TEXT NOT NULL,
|
||||
install_version INTEGER NOT NULL,
|
||||
install_record_digest TEXT NOT NULL,
|
||||
active_lock_digest TEXT,
|
||||
proposer_type TEXT NOT NULL,
|
||||
proposer_id TEXT NOT NULL,
|
||||
confirmer_type TEXT NOT NULL,
|
||||
confirmer_id TEXT NOT NULL,
|
||||
authorization_mode TEXT NOT NULL,
|
||||
reason_code TEXT NOT NULL,
|
||||
occurred_at_ms INTEGER NOT NULL,
|
||||
event_json TEXT NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_quarantine_install_fk
|
||||
FOREIGN KEY (
|
||||
project_id, package_name, installation_id, lock_digest,
|
||||
install_record_digest
|
||||
)
|
||||
REFERENCES "QingLong3PluginPackageInstalls" (
|
||||
project_id, package_name, installation_id, lock_digest, record_digest
|
||||
) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_quarantine_identity_check CHECK (
|
||||
length(mutation_id) BETWEEN 1 AND 128 AND
|
||||
length(package_name) BETWEEN 1 AND 63 AND
|
||||
length(installation_id) BETWEEN 1 AND 128 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
|
||||
length(proposer_id) BETWEEN 1 AND 255 AND
|
||||
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 (
|
||||
length(event_digest) = 64 AND
|
||||
event_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(revocation_receipt_digest) = 64 AND
|
||||
revocation_receipt_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(impact_digest) = 64 AND
|
||||
impact_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(lock_digest) = 64 AND
|
||||
lock_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(install_record_digest) = 64 AND
|
||||
install_record_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
(
|
||||
active_lock_digest IS NULL OR
|
||||
length(active_lock_digest) = 64 AND
|
||||
active_lock_digest NOT GLOB '*[^0-9a-f]*'
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_quarantine_json_check CHECK (
|
||||
length(CAST(event_json AS BLOB)) BETWEEN 2 AND 262144 AND
|
||||
json_valid(event_json) AND json_type(event_json) = 'object' AND
|
||||
json_extract(event_json, '$.schema') =
|
||||
'qinglong/plugin-package-quarantine-event@v1' AND
|
||||
json_extract(event_json, '$.mutationId') = mutation_id AND
|
||||
json_extract(event_json, '$.revocationReceiptDigest') =
|
||||
revocation_receipt_digest AND
|
||||
json_extract(event_json, '$.impactDigest') = impact_digest AND
|
||||
json_extract(event_json, '$.target.projectId') = project_id AND
|
||||
json_extract(event_json, '$.target.packageName') = package_name AND
|
||||
json_extract(event_json, '$.target.installationId') = installation_id AND
|
||||
json_extract(event_json, '$.target.lockDigest') = lock_digest AND
|
||||
json_extract(event_json, '$.target.installState') = install_state AND
|
||||
json_extract(event_json, '$.target.installVersion') = install_version AND
|
||||
json_extract(event_json, '$.target.installRecordDigest') =
|
||||
install_record_digest AND
|
||||
(
|
||||
(
|
||||
active_lock_digest IS NULL AND
|
||||
json_type(event_json, '$.target.activeLockDigest') = 'null'
|
||||
) OR
|
||||
json_extract(event_json, '$.target.activeLockDigest') =
|
||||
active_lock_digest
|
||||
) AND
|
||||
json_extract(event_json, '$.proposer.type') = proposer_type AND
|
||||
json_extract(event_json, '$.proposer.id') = proposer_id AND
|
||||
json_extract(event_json, '$.confirmer.type') = confirmer_type AND
|
||||
json_extract(event_json, '$.confirmer.id') = confirmer_id AND
|
||||
json_extract(event_json, '$.authorizationMode') = authorization_mode AND
|
||||
json_extract(event_json, '$.reasonCode') = reason_code AND
|
||||
json_extract(event_json, '$.occurredAtMs') = occurred_at_ms AND
|
||||
json_extract(event_json, '$.eventDigest') = event_digest
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_quarantine_time_check CHECK (
|
||||
occurred_at_ms >= 0
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_quarantine_mutation_uidx ON "QingLong3PluginPackageQuarantineEvents" (mutation_id)`,
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_quarantine_target_uidx ON "QingLong3PluginPackageQuarantineEvents" (project_id, package_name, installation_id, lock_digest)`,
|
||||
`CREATE INDEX ql3_plugin_package_quarantine_lock_idx ON "QingLong3PluginPackageQuarantineEvents" (lock_digest, project_id, package_name)`,
|
||||
`CREATE INDEX ql3_plugin_package_quarantine_project_idx ON "QingLong3PluginPackageQuarantineEvents" (project_id, package_name, occurred_at_ms, event_digest)`,
|
||||
`
|
||||
CREATE TABLE "QingLong3PluginPackageWithdrawalReceipts" (
|
||||
event_digest TEXT PRIMARY KEY NOT NULL,
|
||||
receipt_digest TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
capability_status TEXT NOT NULL,
|
||||
task_count INTEGER NOT NULL,
|
||||
previous_active_vector_digest TEXT,
|
||||
current_active_vector_digest TEXT,
|
||||
current_tool_snapshot_digest TEXT,
|
||||
retained_source_count INTEGER NOT NULL,
|
||||
committed_at_ms INTEGER NOT NULL,
|
||||
receipt_json TEXT NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_withdrawal_event_fk
|
||||
FOREIGN KEY (event_digest)
|
||||
REFERENCES "QingLong3PluginPackageQuarantineEvents" (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 "QingLong3ProjectToolDefinitionSnapshots" (
|
||||
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 (
|
||||
length(event_digest) = 64 AND
|
||||
event_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(receipt_digest) = 64 AND
|
||||
receipt_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
(
|
||||
previous_active_vector_digest IS NULL OR
|
||||
length(previous_active_vector_digest) = 64 AND
|
||||
previous_active_vector_digest NOT GLOB '*[^0-9a-f]*'
|
||||
) AND
|
||||
(
|
||||
current_active_vector_digest IS NULL OR
|
||||
length(current_active_vector_digest) = 64 AND
|
||||
current_active_vector_digest NOT GLOB '*[^0-9a-f]*'
|
||||
) AND
|
||||
(
|
||||
current_tool_snapshot_digest IS NULL OR
|
||||
length(current_tool_snapshot_digest) = 64 AND
|
||||
current_tool_snapshot_digest NOT GLOB '*[^0-9a-f]*'
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_withdrawal_json_check CHECK (
|
||||
length(CAST(receipt_json AS BLOB)) BETWEEN 2 AND 8388608 AND
|
||||
json_valid(receipt_json) AND json_type(receipt_json) = 'object' AND
|
||||
json_extract(receipt_json, '$.schema') =
|
||||
'qinglong/plugin-package-withdrawal-receipt@v1' AND
|
||||
json_extract(receipt_json, '$.eventDigest') = event_digest AND
|
||||
json_extract(receipt_json, '$.target.projectId') = project_id AND
|
||||
json_extract(receipt_json, '$.capability.status') = capability_status AND
|
||||
json_array_length(
|
||||
json_extract(receipt_json, '$.capability.taskWithdrawals')
|
||||
) = task_count AND
|
||||
(
|
||||
(
|
||||
previous_active_vector_digest IS NULL AND
|
||||
json_type(
|
||||
receipt_json, '$.capability.previousActiveVectorDigest'
|
||||
) = 'null'
|
||||
) OR
|
||||
json_extract(
|
||||
receipt_json, '$.capability.previousActiveVectorDigest'
|
||||
) = previous_active_vector_digest
|
||||
) AND
|
||||
(
|
||||
(
|
||||
current_active_vector_digest IS NULL AND
|
||||
json_type(
|
||||
receipt_json, '$.capability.currentActiveVectorDigest'
|
||||
) = 'null'
|
||||
) OR
|
||||
json_extract(
|
||||
receipt_json, '$.capability.currentActiveVectorDigest'
|
||||
) = current_active_vector_digest
|
||||
) AND
|
||||
(
|
||||
(
|
||||
current_tool_snapshot_digest IS NULL AND
|
||||
json_type(
|
||||
receipt_json, '$.capability.currentToolSnapshotDigest'
|
||||
) = 'null'
|
||||
) OR
|
||||
json_extract(
|
||||
receipt_json, '$.capability.currentToolSnapshotDigest'
|
||||
) = current_tool_snapshot_digest
|
||||
) AND
|
||||
json_extract(receipt_json, '$.capability.retainedSourceCount') =
|
||||
retained_source_count AND
|
||||
json_extract(receipt_json, '$.committedAtMs') = committed_at_ms AND
|
||||
json_extract(receipt_json, '$.receiptDigest') = receipt_digest
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_withdrawal_time_check CHECK (
|
||||
committed_at_ms >= 0
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_withdrawal_receipt_uidx ON "QingLong3PluginPackageWithdrawalReceipts" (receipt_digest)`,
|
||||
`CREATE INDEX ql3_plugin_package_withdrawal_snapshot_idx ON "QingLong3PluginPackageWithdrawalReceipts" (current_tool_snapshot_digest, event_digest)`,
|
||||
`
|
||||
CREATE TABLE "QingLong3PluginPackageWithdrawalTasks" (
|
||||
event_digest TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
task_id TEXT NOT NULL,
|
||||
previous_revision INTEGER NOT NULL,
|
||||
disabled_revision INTEGER NOT NULL,
|
||||
previous_content_digest TEXT NOT NULL,
|
||||
disabled_content_digest TEXT NOT NULL,
|
||||
PRIMARY KEY (event_digest, task_id),
|
||||
CONSTRAINT ql3_plugin_package_withdrawal_task_receipt_fk
|
||||
FOREIGN KEY (event_digest)
|
||||
REFERENCES "QingLong3PluginPackageWithdrawalReceipts" (event_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_withdrawal_task_previous_fk
|
||||
FOREIGN KEY (project_id, task_id, previous_revision)
|
||||
REFERENCES "QingLong3TaskDefinitionRevisions" (
|
||||
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 "QingLong3TaskDefinitionRevisions" (
|
||||
project_id, task_id, revision
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_withdrawal_task_identity_check CHECK (
|
||||
length(task_id) BETWEEN 1 AND 128 AND
|
||||
previous_revision BETWEEN 1 AND 2147483646 AND
|
||||
disabled_revision = previous_revision + 1
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_withdrawal_task_digest_check CHECK (
|
||||
length(event_digest) = 64 AND
|
||||
event_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(previous_content_digest) = 64 AND
|
||||
previous_content_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(disabled_content_digest) = 64 AND
|
||||
disabled_content_digest NOT GLOB '*[^0-9a-f]*'
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE INDEX ql3_plugin_package_withdrawal_task_task_idx ON "QingLong3PluginPackageWithdrawalTasks" (project_id, task_id, event_digest)`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V34 =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1}';
|
||||
const CAPABILITIES_V35 =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"plugin_package_quarantine":1}';
|
||||
|
||||
export const local0070CapabilityV35Migration = defineLocalSqliteMigration({
|
||||
id: '0070-capability-v35',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 35,
|
||||
migration_id = '0069-plugin-package-quarantine',
|
||||
capabilities = '${CAPABILITIES_V35}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 34
|
||||
AND migration_id = '0067-tool-result-rekey-overlays'
|
||||
AND capabilities = '${CAPABILITIES_V34}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0071LocalIdentityCredentialAdministrationMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0071-local-identity-credential-administration',
|
||||
statements: [
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_local_credential_pepper_binding_triple_uidx"
|
||||
ON "QingLong3ApiCredentialPepperBindings" (
|
||||
"credential_id", "credential_version", "pepper_key_id"
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE TABLE "QingLong3IdentityAdministrationMutations" (
|
||||
"mutation_id" TEXT PRIMARY KEY NOT NULL,
|
||||
"project_id" TEXT NOT NULL,
|
||||
"operation" TEXT NOT NULL,
|
||||
"subject_type" TEXT NOT NULL,
|
||||
"subject_id" TEXT NOT NULL,
|
||||
"subject_version" INTEGER NOT NULL,
|
||||
"expected_previous_version" INTEGER NOT NULL,
|
||||
"status" TEXT NOT NULL,
|
||||
"changed_by_type" TEXT NOT NULL,
|
||||
"changed_by_id" TEXT NOT NULL,
|
||||
"audit_event_id" TEXT NOT NULL UNIQUE,
|
||||
"identity_created_at_ms" INTEGER NOT NULL,
|
||||
"created_at_ms" INTEGER NOT NULL,
|
||||
FOREIGN KEY ("project_id")
|
||||
REFERENCES "QingLong3Projects" ("id") ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("subject_type", "subject_id")
|
||||
REFERENCES "QingLong3IdentitySubjects" ("subject_type", "subject_id")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("audit_event_id")
|
||||
REFERENCES "QingLong3SecurityAuditEvents" ("event_id")
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_identity_admin_mutation_id_check CHECK (
|
||||
length("mutation_id") = 36
|
||||
),
|
||||
CONSTRAINT ql3_identity_admin_operation_check CHECK (
|
||||
"operation" IN ('register','enable','disable')
|
||||
),
|
||||
CONSTRAINT ql3_identity_admin_subject_check CHECK (
|
||||
"subject_type" IN ('user','api_app','mcp_client','agent')
|
||||
AND length("subject_id") BETWEEN 1 AND 255
|
||||
),
|
||||
CONSTRAINT ql3_identity_admin_transition_check CHECK (
|
||||
"subject_version" = "expected_previous_version" + 1
|
||||
AND "subject_version" BETWEEN 1 AND 2147483647
|
||||
AND "expected_previous_version" BETWEEN 0 AND 2147483646
|
||||
AND (
|
||||
("operation" = 'register'
|
||||
AND "expected_previous_version" = 0 AND "status" = 'active')
|
||||
OR ("operation" = 'enable'
|
||||
AND "expected_previous_version" > 0 AND "status" = 'active')
|
||||
OR ("operation" = 'disable'
|
||||
AND "expected_previous_version" > 0 AND "status" = 'disabled')
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_identity_admin_actor_check CHECK (
|
||||
"changed_by_type" = 'user'
|
||||
AND length("changed_by_id") BETWEEN 1 AND 255
|
||||
),
|
||||
CONSTRAINT ql3_identity_admin_audit_check CHECK (
|
||||
"audit_event_id" = "mutation_id"
|
||||
),
|
||||
CONSTRAINT ql3_identity_admin_time_check CHECK (
|
||||
"identity_created_at_ms" >= 0
|
||||
AND "created_at_ms" >= "identity_created_at_ms"
|
||||
)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_identity_admin_subject_idx"
|
||||
ON "QingLong3IdentityAdministrationMutations" (
|
||||
"subject_type", "subject_id", "subject_version" DESC
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE TABLE "QingLong3ApiCredentialAdministrationMutations" (
|
||||
"mutation_id" TEXT PRIMARY KEY NOT NULL,
|
||||
"project_id" TEXT NOT NULL,
|
||||
"operation" TEXT NOT NULL,
|
||||
"credential_id" TEXT NOT NULL,
|
||||
"credential_version" INTEGER NOT NULL,
|
||||
"expected_previous_version" INTEGER NOT NULL,
|
||||
"subject_type" TEXT NOT NULL,
|
||||
"subject_id" TEXT NOT NULL,
|
||||
"subject_status" TEXT NOT NULL,
|
||||
"state" TEXT NOT NULL,
|
||||
"pepper_key_id" TEXT NOT NULL,
|
||||
"secret_digest" TEXT NOT NULL,
|
||||
"not_before_at_ms" INTEGER NOT NULL,
|
||||
"expires_at_ms" INTEGER NOT NULL,
|
||||
"delivery_digest" TEXT,
|
||||
"changed_by_type" TEXT NOT NULL,
|
||||
"changed_by_id" TEXT NOT NULL,
|
||||
"audit_event_id" TEXT NOT NULL UNIQUE,
|
||||
"created_at_ms" INTEGER NOT NULL,
|
||||
FOREIGN KEY ("project_id")
|
||||
REFERENCES "QingLong3Projects" ("id") ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("credential_id", "credential_version")
|
||||
REFERENCES "QingLong3ApiCredentials" ("credential_id", "version")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("credential_id", "credential_version", "pepper_key_id")
|
||||
REFERENCES "QingLong3ApiCredentialPepperBindings" (
|
||||
"credential_id", "credential_version", "pepper_key_id"
|
||||
) ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("audit_event_id")
|
||||
REFERENCES "QingLong3SecurityAuditEvents" ("event_id")
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_credential_admin_mutation_id_check CHECK (
|
||||
length("mutation_id") = 36
|
||||
),
|
||||
CONSTRAINT ql3_credential_admin_operation_check CHECK (
|
||||
"operation" IN ('issue','rotate','revoke')
|
||||
),
|
||||
CONSTRAINT ql3_credential_admin_identity_check CHECK (
|
||||
length("credential_id") BETWEEN 1 AND 64
|
||||
AND "credential_id" NOT GLOB '*[^A-Za-z0-9._:-]*'
|
||||
AND "subject_type" IN ('user','api_app','mcp_client','agent')
|
||||
AND length("subject_id") BETWEEN 1 AND 255
|
||||
AND "subject_status" IN ('active','disabled')
|
||||
),
|
||||
CONSTRAINT ql3_credential_admin_transition_check CHECK (
|
||||
"credential_version" = "expected_previous_version" + 1
|
||||
AND "credential_version" BETWEEN 1 AND 2147483647
|
||||
AND "expected_previous_version" BETWEEN 0 AND 2147483646
|
||||
AND (
|
||||
("operation" = 'issue'
|
||||
AND "expected_previous_version" = 0 AND "state" = 'active')
|
||||
OR ("operation" = 'rotate'
|
||||
AND "expected_previous_version" > 0 AND "state" = 'active')
|
||||
OR ("operation" = 'revoke'
|
||||
AND "expected_previous_version" > 0 AND "state" = 'revoked')
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_credential_admin_digest_check CHECK (
|
||||
length("pepper_key_id") BETWEEN 1 AND 64
|
||||
AND "pepper_key_id" NOT GLOB '*[^A-Za-z0-9._:-]*'
|
||||
AND length("secret_digest") = 64
|
||||
AND "secret_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
AND (
|
||||
("operation" IN ('issue','rotate')
|
||||
AND length("delivery_digest") = 64
|
||||
AND "delivery_digest" NOT GLOB '*[^0-9a-f]*')
|
||||
OR ("operation" = 'revoke' AND "delivery_digest" IS NULL)
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_credential_admin_lifetime_check CHECK (
|
||||
"created_at_ms" >= 0
|
||||
AND "not_before_at_ms" >= "created_at_ms"
|
||||
AND "expires_at_ms" > "not_before_at_ms"
|
||||
),
|
||||
CONSTRAINT ql3_credential_admin_actor_check CHECK (
|
||||
"changed_by_type" = 'user'
|
||||
AND length("changed_by_id") BETWEEN 1 AND 255
|
||||
),
|
||||
CONSTRAINT ql3_credential_admin_audit_check CHECK (
|
||||
"audit_event_id" = "mutation_id"
|
||||
)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_credential_admin_credential_idx"
|
||||
ON "QingLong3ApiCredentialAdministrationMutations" (
|
||||
"credential_id", "credential_version" DESC
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_credential_admin_subject_idx"
|
||||
ON "QingLong3ApiCredentialAdministrationMutations" (
|
||||
"subject_type", "subject_id", "created_at_ms" DESC
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE TABLE "QingLong3ApiCredentialDeliveryAcknowledgements" (
|
||||
"credential_mutation_id" TEXT PRIMARY KEY NOT NULL,
|
||||
"acknowledgement_mutation_id" TEXT NOT NULL UNIQUE,
|
||||
"project_id" TEXT NOT NULL,
|
||||
"delivery_digest" TEXT NOT NULL,
|
||||
"acknowledged_by_type" TEXT NOT NULL,
|
||||
"acknowledged_by_id" TEXT NOT NULL,
|
||||
"audit_event_id" TEXT NOT NULL UNIQUE,
|
||||
"acknowledged_at_ms" INTEGER NOT NULL,
|
||||
FOREIGN KEY ("credential_mutation_id")
|
||||
REFERENCES "QingLong3ApiCredentialAdministrationMutations" ("mutation_id")
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("project_id")
|
||||
REFERENCES "QingLong3Projects" ("id") ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("audit_event_id")
|
||||
REFERENCES "QingLong3SecurityAuditEvents" ("event_id")
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_credential_delivery_ack_identity_check CHECK (
|
||||
length("credential_mutation_id") = 36
|
||||
AND length("acknowledgement_mutation_id") = 36
|
||||
AND "credential_mutation_id" <> "acknowledgement_mutation_id"
|
||||
),
|
||||
CONSTRAINT ql3_credential_delivery_ack_digest_check CHECK (
|
||||
length("delivery_digest") = 64
|
||||
AND "delivery_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_credential_delivery_ack_actor_check CHECK (
|
||||
"acknowledged_by_type" = 'user'
|
||||
AND length("acknowledged_by_id") BETWEEN 1 AND 255
|
||||
),
|
||||
CONSTRAINT ql3_credential_delivery_ack_audit_check CHECK (
|
||||
"audit_event_id" = "acknowledgement_mutation_id"
|
||||
),
|
||||
CONSTRAINT ql3_credential_delivery_ack_time_check CHECK (
|
||||
"acknowledged_at_ms" >= 0
|
||||
)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_credential_delivery_ack_project_idx"
|
||||
ON "QingLong3ApiCredentialDeliveryAcknowledgements" (
|
||||
"project_id", "acknowledged_at_ms" DESC
|
||||
)
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V35 =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"plugin_package_quarantine":1}';
|
||||
const CAPABILITIES_V36 =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_identity_credential_administration":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"plugin_package_quarantine":1}';
|
||||
|
||||
export const local0072CapabilityV36Migration = defineLocalSqliteMigration({
|
||||
id: '0072-capability-v36',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 36,
|
||||
migration_id = '0071-local-identity-credential-administration',
|
||||
capabilities = '${CAPABILITIES_V36}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 35
|
||||
AND migration_id = '0069-plugin-package-quarantine'
|
||||
AND capabilities = '${CAPABILITIES_V35}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0073LocalProjectAdministrationMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0073-local-project-administration',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3ProjectAdministrationMutations" (
|
||||
"mutation_id" TEXT PRIMARY KEY NOT NULL,
|
||||
"operation" TEXT NOT NULL,
|
||||
"authority_project_id" TEXT NOT NULL,
|
||||
"project_id" TEXT NOT NULL,
|
||||
"project_name" TEXT NOT NULL,
|
||||
"project_slug" TEXT NOT NULL,
|
||||
"project_status" TEXT NOT NULL,
|
||||
"project_version" INTEGER NOT NULL,
|
||||
"expected_previous_version" INTEGER NOT NULL,
|
||||
"changed_by_type" TEXT NOT NULL,
|
||||
"changed_by_id" TEXT NOT NULL,
|
||||
"initial_owner_binding_version" INTEGER,
|
||||
"audit_event_id" TEXT NOT NULL UNIQUE,
|
||||
"project_created_at_ms" INTEGER NOT NULL,
|
||||
"created_at_ms" INTEGER NOT NULL,
|
||||
FOREIGN KEY ("authority_project_id")
|
||||
REFERENCES "QingLong3Projects" ("id") ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("project_id")
|
||||
REFERENCES "QingLong3Projects" ("id") ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("audit_event_id")
|
||||
REFERENCES "QingLong3SecurityAuditEvents" ("event_id")
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_project_admin_mutation_id_check CHECK (
|
||||
length("mutation_id") = 36
|
||||
AND substr("mutation_id", 9, 1) = '-'
|
||||
AND substr("mutation_id", 14, 1) = '-'
|
||||
AND substr("mutation_id", 19, 1) = '-'
|
||||
AND substr("mutation_id", 24, 1) = '-'
|
||||
AND replace("mutation_id", '-', '') NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_project_admin_operation_check CHECK (
|
||||
"operation" IN ('create','archive','restore')
|
||||
),
|
||||
CONSTRAINT ql3_project_admin_identity_check CHECK (
|
||||
length("authority_project_id") BETWEEN 1 AND 128
|
||||
AND length("project_id") BETWEEN 1 AND 128
|
||||
AND length("project_name") BETWEEN 1 AND 255
|
||||
AND length("project_slug") BETWEEN 1 AND 128
|
||||
AND "project_slug" = lower("project_slug")
|
||||
AND "project_slug" NOT GLOB '*[^a-z0-9-]*'
|
||||
AND substr("project_slug", 1, 1) NOT GLOB '[^a-z0-9]'
|
||||
AND substr("project_slug", -1, 1) NOT GLOB '[^a-z0-9]'
|
||||
),
|
||||
CONSTRAINT ql3_project_admin_transition_check CHECK (
|
||||
"project_version" = "expected_previous_version" + 1
|
||||
AND "project_version" BETWEEN 1 AND 2147483647
|
||||
AND "expected_previous_version" BETWEEN 0 AND 2147483646
|
||||
AND (
|
||||
("operation" = 'create'
|
||||
AND "expected_previous_version" = 0
|
||||
AND "project_status" = 'active'
|
||||
AND "initial_owner_binding_version" = 1)
|
||||
OR ("operation" = 'archive'
|
||||
AND "expected_previous_version" > 0
|
||||
AND "project_status" = 'archived'
|
||||
AND "initial_owner_binding_version" IS NULL)
|
||||
OR ("operation" = 'restore'
|
||||
AND "expected_previous_version" > 0
|
||||
AND "project_status" = 'active'
|
||||
AND "initial_owner_binding_version" IS NULL)
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_project_admin_actor_check CHECK (
|
||||
"changed_by_type" = 'user'
|
||||
AND length("changed_by_id") BETWEEN 1 AND 255
|
||||
),
|
||||
CONSTRAINT ql3_project_admin_audit_check CHECK (
|
||||
"audit_event_id" = "mutation_id"
|
||||
),
|
||||
CONSTRAINT ql3_project_admin_time_check CHECK (
|
||||
"project_created_at_ms" >= 0
|
||||
AND "created_at_ms" >= "project_created_at_ms"
|
||||
)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_project_admin_project_version_uidx"
|
||||
ON "QingLong3ProjectAdministrationMutations" (
|
||||
"project_id", "project_version"
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_project_admin_authority_time_idx"
|
||||
ON "QingLong3ProjectAdministrationMutations" (
|
||||
"authority_project_id", "created_at_ms" DESC, "mutation_id" DESC
|
||||
)
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V36 =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_identity_credential_administration":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"plugin_package_quarantine":1}';
|
||||
const CAPABILITIES_V37 =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_project_administration":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_identity_credential_administration":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"plugin_package_quarantine":1}';
|
||||
|
||||
export const local0074CapabilityV37Migration = defineLocalSqliteMigration({
|
||||
id: '0074-capability-v37',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 37,
|
||||
migration_id = '0073-local-project-administration',
|
||||
capabilities = '${CAPABILITIES_V37}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 36
|
||||
AND migration_id = '0071-local-identity-credential-administration'
|
||||
AND capabilities = '${CAPABILITIES_V36}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0075SecurityAuditCompactionsMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0075-security-audit-compactions',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3SecurityAuditCompactions" (
|
||||
"mutation_id" TEXT PRIMARY KEY NOT NULL,
|
||||
"request_id" TEXT NOT NULL,
|
||||
"authority_project_id" TEXT NOT NULL,
|
||||
"retention_ms" INTEGER NOT NULL,
|
||||
"eligible_before_ms" INTEGER NOT NULL,
|
||||
"batch_limit" INTEGER NOT NULL,
|
||||
"deleted_count" INTEGER NOT NULL,
|
||||
"deleted_payload_bytes" INTEGER NOT NULL,
|
||||
"first_occurred_at_ms" INTEGER,
|
||||
"first_event_id" TEXT,
|
||||
"last_occurred_at_ms" INTEGER,
|
||||
"last_event_id" TEXT,
|
||||
"records_digest" TEXT NOT NULL,
|
||||
"audit_event_id" TEXT NOT NULL UNIQUE,
|
||||
"created_at_ms" INTEGER NOT NULL,
|
||||
FOREIGN KEY ("authority_project_id")
|
||||
REFERENCES "QingLong3Projects" ("id") ON DELETE RESTRICT,
|
||||
FOREIGN KEY ("audit_event_id")
|
||||
REFERENCES "QingLong3SecurityAuditEvents" ("event_id")
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_audit_compaction_mutation_check CHECK (
|
||||
length("mutation_id") = 36
|
||||
AND substr("mutation_id", 9, 1) = '-'
|
||||
AND substr("mutation_id", 14, 1) = '-'
|
||||
AND substr("mutation_id", 19, 1) = '-'
|
||||
AND substr("mutation_id", 24, 1) = '-'
|
||||
AND replace("mutation_id", '-', '') NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_audit_compaction_request_check CHECK (
|
||||
length("request_id") BETWEEN 1 AND 128
|
||||
),
|
||||
CONSTRAINT ql3_audit_compaction_authority_check CHECK (
|
||||
length("authority_project_id") BETWEEN 1 AND 128
|
||||
),
|
||||
CONSTRAINT ql3_audit_compaction_policy_check CHECK (
|
||||
"retention_ms" BETWEEN 2592000000 AND 315360000000
|
||||
AND "eligible_before_ms" >= 0
|
||||
AND "eligible_before_ms" + "retention_ms" <= "created_at_ms"
|
||||
AND "batch_limit" BETWEEN 1 AND 512
|
||||
),
|
||||
CONSTRAINT ql3_audit_compaction_result_check CHECK (
|
||||
"deleted_count" BETWEEN 0 AND "batch_limit"
|
||||
AND "deleted_payload_bytes" BETWEEN 0 AND 16777216
|
||||
AND length("records_digest") = 64
|
||||
AND "records_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
AND (
|
||||
("deleted_count" = 0
|
||||
AND "deleted_payload_bytes" = 0
|
||||
AND "first_occurred_at_ms" IS NULL
|
||||
AND "first_event_id" IS NULL
|
||||
AND "last_occurred_at_ms" IS NULL
|
||||
AND "last_event_id" IS NULL)
|
||||
OR ("deleted_count" > 0
|
||||
AND "deleted_payload_bytes" > 0
|
||||
AND "first_occurred_at_ms" >= 0
|
||||
AND "last_occurred_at_ms" >= "first_occurred_at_ms"
|
||||
AND length("first_event_id") = 36
|
||||
AND length("last_event_id") = 36)
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_audit_compaction_audit_check CHECK (
|
||||
"audit_event_id" = "mutation_id"
|
||||
),
|
||||
CONSTRAINT ql3_audit_compaction_time_check CHECK (
|
||||
"created_at_ms" >= 2592000000
|
||||
)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE INDEX "ql3_audit_compaction_authority_time_idx"
|
||||
ON "QingLong3SecurityAuditCompactions" (
|
||||
"authority_project_id", "created_at_ms" DESC, "mutation_id" DESC
|
||||
)
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V37 =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_project_administration":1,"local_security_audit":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_identity_credential_administration":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"plugin_package_quarantine":1}';
|
||||
const CAPABILITIES_V38 =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_project_administration":1,"local_security_audit":1,"local_security_audit_compaction":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_identity_credential_administration":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"plugin_package_quarantine":1}';
|
||||
|
||||
export const local0076CapabilityV38Migration = defineLocalSqliteMigration({
|
||||
id: '0076-capability-v38',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 38,
|
||||
migration_id = '0075-security-audit-compactions',
|
||||
capabilities = '${CAPABILITIES_V38}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 37
|
||||
AND migration_id = '0073-local-project-administration'
|
||||
AND capabilities = '${CAPABILITIES_V37}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,349 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0077PluginPackageLifecycleMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0077-plugin-package-lifecycle',
|
||||
statements: [
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_approved_action_dispatch_lifecycle_uidx
|
||||
ON "QingLong3ApprovedActionDispatches" (
|
||||
dispatch_id, project_id, action_type, action_digest, preview_digest
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE TABLE "QingLong3PluginPackageLifecycleEvents" (
|
||||
event_digest TEXT PRIMARY KEY NOT NULL,
|
||||
mutation_id TEXT NOT NULL,
|
||||
dispatch_id TEXT NOT NULL,
|
||||
approved_action_type TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
package_name TEXT NOT NULL,
|
||||
installation_id TEXT NOT NULL,
|
||||
lock_digest TEXT NOT NULL,
|
||||
install_version INTEGER NOT NULL,
|
||||
install_record_digest TEXT NOT NULL,
|
||||
expected_version INTEGER NOT NULL,
|
||||
expected_disposition TEXT NOT NULL,
|
||||
expected_event_digest TEXT,
|
||||
generation_digest TEXT NOT NULL,
|
||||
materialized_revision_digest TEXT NOT NULL,
|
||||
current_tool_snapshot_digest TEXT NOT NULL,
|
||||
reference_graph_digest TEXT NOT NULL,
|
||||
impact_digest TEXT NOT NULL,
|
||||
action_digest TEXT NOT NULL,
|
||||
requested_by_type TEXT NOT NULL,
|
||||
requested_by_id TEXT NOT NULL,
|
||||
approved_by_type TEXT NOT NULL,
|
||||
approved_by_id TEXT NOT NULL,
|
||||
authorization_mode TEXT NOT NULL,
|
||||
occurred_at_ms INTEGER NOT NULL,
|
||||
event_json TEXT NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_install_fk
|
||||
FOREIGN KEY (
|
||||
project_id, package_name, installation_id, lock_digest,
|
||||
install_record_digest
|
||||
)
|
||||
REFERENCES "QingLong3PluginPackageInstalls" (
|
||||
project_id, package_name, installation_id, lock_digest, record_digest
|
||||
) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_dispatch_fk
|
||||
FOREIGN KEY (
|
||||
dispatch_id, project_id, approved_action_type, action_digest,
|
||||
impact_digest
|
||||
)
|
||||
REFERENCES "QingLong3ApprovedActionDispatches" (
|
||||
dispatch_id, project_id, action_type, action_digest, preview_digest
|
||||
) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_previous_event_fk
|
||||
FOREIGN KEY (expected_event_digest)
|
||||
REFERENCES "QingLong3PluginPackageLifecycleEvents" (event_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_identity_check CHECK (
|
||||
length(mutation_id) BETWEEN 1 AND 128 AND
|
||||
length(dispatch_id) BETWEEN 1 AND 128 AND
|
||||
length(package_name) BETWEEN 1 AND 63 AND
|
||||
length(installation_id) BETWEEN 1 AND 128 AND
|
||||
install_version BETWEEN 1 AND 2147483647 AND
|
||||
action IN ('disable','enable','uninstall') AND
|
||||
approved_action_type = 'plugin_package.lifecycle.' || action
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_expectation_check CHECK (
|
||||
(
|
||||
action = 'disable' AND expected_disposition = 'active'
|
||||
) OR (
|
||||
action IN ('enable','uninstall') AND
|
||||
expected_disposition = 'disabled'
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_origin_check CHECK (
|
||||
(
|
||||
expected_version = 0 AND
|
||||
expected_disposition = 'active' AND
|
||||
expected_event_digest IS NULL
|
||||
) OR (
|
||||
expected_version BETWEEN 1 AND 2147483646 AND
|
||||
expected_event_digest IS NOT NULL
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_subject_check CHECK (
|
||||
requested_by_type = 'user' AND approved_by_type = 'user' AND
|
||||
length(requested_by_id) BETWEEN 1 AND 255 AND
|
||||
length(approved_by_id) BETWEEN 1 AND 255 AND
|
||||
authorization_mode IN ('human_confirmation','separation_of_duty') AND
|
||||
(
|
||||
(
|
||||
authorization_mode = 'human_confirmation' AND
|
||||
requested_by_id = approved_by_id
|
||||
) OR (
|
||||
authorization_mode = 'separation_of_duty' AND
|
||||
requested_by_id <> approved_by_id
|
||||
)
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_digest_check CHECK (
|
||||
length(event_digest) = 64 AND
|
||||
event_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(lock_digest) = 64 AND
|
||||
lock_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(install_record_digest) = 64 AND
|
||||
install_record_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(generation_digest) = 64 AND
|
||||
generation_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(materialized_revision_digest) = 64 AND
|
||||
materialized_revision_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(current_tool_snapshot_digest) = 64 AND
|
||||
current_tool_snapshot_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(reference_graph_digest) = 64 AND
|
||||
reference_graph_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(impact_digest) = 64 AND
|
||||
impact_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(action_digest) = 64 AND
|
||||
action_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
(
|
||||
expected_event_digest IS NULL OR
|
||||
length(expected_event_digest) = 64 AND
|
||||
expected_event_digest NOT GLOB '*[^0-9a-f]*'
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_json_check CHECK (
|
||||
length(CAST(event_json AS BLOB)) BETWEEN 2 AND 524288 AND
|
||||
json_valid(event_json) AND json_type(event_json) = 'object' AND
|
||||
json_extract(event_json, '$.schema') =
|
||||
'qinglong/plugin-package-lifecycle-event@v1' AND
|
||||
json_extract(event_json, '$.mutationId') = mutation_id AND
|
||||
json_extract(event_json, '$.dispatchId') = dispatch_id AND
|
||||
json_extract(event_json, '$.impact.schema') =
|
||||
'qinglong/plugin-package-lifecycle-impact@v1' AND
|
||||
json_extract(event_json, '$.impact.action') = action AND
|
||||
json_extract(event_json, '$.impact.target.projectId') = project_id AND
|
||||
json_extract(event_json, '$.impact.target.packageName') = package_name AND
|
||||
json_extract(event_json, '$.impact.target.installationId') =
|
||||
installation_id AND
|
||||
json_extract(event_json, '$.impact.target.lockDigest') = lock_digest AND
|
||||
json_extract(event_json, '$.impact.target.installVersion') =
|
||||
install_version AND
|
||||
json_extract(event_json, '$.impact.target.installRecordDigest') =
|
||||
install_record_digest AND
|
||||
json_extract(event_json, '$.impact.expected.version') = expected_version AND
|
||||
json_extract(event_json, '$.impact.expected.disposition') =
|
||||
expected_disposition AND
|
||||
(
|
||||
(
|
||||
expected_event_digest IS NULL AND
|
||||
json_type(event_json, '$.impact.expected.eventDigest') = 'null'
|
||||
) OR
|
||||
json_extract(event_json, '$.impact.expected.eventDigest') =
|
||||
expected_event_digest
|
||||
) AND
|
||||
json_extract(event_json, '$.impact.generationDigest') =
|
||||
generation_digest AND
|
||||
json_extract(event_json, '$.impact.materializedRevisionDigest') =
|
||||
materialized_revision_digest AND
|
||||
json_extract(event_json, '$.impact.currentToolSnapshotDigest') =
|
||||
current_tool_snapshot_digest AND
|
||||
json_extract(event_json, '$.impact.referenceGraphDigest') =
|
||||
reference_graph_digest AND
|
||||
json_extract(event_json, '$.impact.impactDigest') = impact_digest AND
|
||||
json_extract(event_json, '$.actionDigest') = action_digest AND
|
||||
json_extract(event_json, '$.requestedBy.type') = requested_by_type AND
|
||||
json_extract(event_json, '$.requestedBy.id') = requested_by_id AND
|
||||
json_extract(event_json, '$.approvedBy.type') = approved_by_type AND
|
||||
json_extract(event_json, '$.approvedBy.id') = approved_by_id AND
|
||||
json_extract(event_json, '$.authorizationMode') = authorization_mode AND
|
||||
json_extract(event_json, '$.occurredAtMs') = occurred_at_ms AND
|
||||
json_extract(event_json, '$.eventDigest') = event_digest
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_time_check CHECK (
|
||||
occurred_at_ms >= 0
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_lifecycle_mutation_uidx ON "QingLong3PluginPackageLifecycleEvents" (mutation_id)`,
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_lifecycle_dispatch_uidx ON "QingLong3PluginPackageLifecycleEvents" (dispatch_id)`,
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_lifecycle_target_version_uidx ON "QingLong3PluginPackageLifecycleEvents" (project_id, package_name, installation_id, lock_digest, expected_version)`,
|
||||
`CREATE INDEX ql3_plugin_package_lifecycle_project_idx ON "QingLong3PluginPackageLifecycleEvents" (project_id, package_name, occurred_at_ms, event_digest)`,
|
||||
`
|
||||
CREATE TABLE "QingLong3PluginPackageLifecycleHeads" (
|
||||
project_id TEXT NOT NULL,
|
||||
package_name TEXT NOT NULL,
|
||||
installation_id TEXT NOT NULL,
|
||||
lock_digest TEXT NOT NULL,
|
||||
install_record_digest TEXT NOT NULL,
|
||||
version INTEGER NOT NULL,
|
||||
disposition TEXT NOT NULL,
|
||||
event_digest TEXT NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
PRIMARY KEY (project_id, package_name),
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_head_install_fk
|
||||
FOREIGN KEY (
|
||||
project_id, package_name, installation_id, lock_digest,
|
||||
install_record_digest
|
||||
)
|
||||
REFERENCES "QingLong3PluginPackageInstalls" (
|
||||
project_id, package_name, installation_id, lock_digest, record_digest
|
||||
) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_head_event_fk
|
||||
FOREIGN KEY (event_digest)
|
||||
REFERENCES "QingLong3PluginPackageLifecycleEvents" (event_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_head_state_check CHECK (
|
||||
version BETWEEN 1 AND 2147483647 AND
|
||||
disposition IN ('active','disabled','uninstalled') AND
|
||||
updated_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_head_digest_check CHECK (
|
||||
length(lock_digest) = 64 AND
|
||||
lock_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(install_record_digest) = 64 AND
|
||||
install_record_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(event_digest) = 64 AND
|
||||
event_digest NOT GLOB '*[^0-9a-f]*'
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_lifecycle_head_event_uidx ON "QingLong3PluginPackageLifecycleHeads" (event_digest)`,
|
||||
`
|
||||
CREATE TABLE "QingLong3PluginPackageLifecycleReceipts" (
|
||||
event_digest TEXT PRIMARY KEY NOT NULL,
|
||||
receipt_digest TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
capability_status TEXT NOT NULL,
|
||||
task_count INTEGER NOT NULL,
|
||||
previous_active_vector_digest TEXT NOT NULL,
|
||||
current_active_vector_digest TEXT NOT NULL,
|
||||
current_tool_snapshot_digest TEXT NOT NULL,
|
||||
retained_source_count INTEGER NOT NULL,
|
||||
committed_at_ms INTEGER NOT NULL,
|
||||
receipt_json TEXT NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_receipt_event_fk
|
||||
FOREIGN KEY (event_digest)
|
||||
REFERENCES "QingLong3PluginPackageLifecycleEvents" (event_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_receipt_snapshot_fk
|
||||
FOREIGN KEY (
|
||||
project_id, current_active_vector_digest, current_tool_snapshot_digest
|
||||
)
|
||||
REFERENCES "QingLong3ProjectToolDefinitionSnapshots" (
|
||||
project_id, active_vector_digest, snapshot_digest
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_receipt_state_check CHECK (
|
||||
(
|
||||
action = 'disable' AND capability_status = 'withdrawn' AND
|
||||
previous_active_vector_digest <> current_active_vector_digest
|
||||
) OR (
|
||||
action = 'enable' AND capability_status = 'restored' AND
|
||||
previous_active_vector_digest <> current_active_vector_digest
|
||||
) OR (
|
||||
action = 'uninstall' AND capability_status = 'retired' AND
|
||||
task_count = 0 AND
|
||||
previous_active_vector_digest = current_active_vector_digest
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_receipt_bounds_check CHECK (
|
||||
task_count BETWEEN 0 AND 128 AND
|
||||
retained_source_count BETWEEN 0 AND 128 AND
|
||||
committed_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_receipt_digest_check CHECK (
|
||||
length(receipt_digest) = 64 AND
|
||||
receipt_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(previous_active_vector_digest) = 64 AND
|
||||
previous_active_vector_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(current_active_vector_digest) = 64 AND
|
||||
current_active_vector_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(current_tool_snapshot_digest) = 64 AND
|
||||
current_tool_snapshot_digest NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_receipt_json_check CHECK (
|
||||
length(CAST(receipt_json AS BLOB)) BETWEEN 2 AND 524288 AND
|
||||
json_valid(receipt_json) AND json_type(receipt_json) = 'object' AND
|
||||
json_extract(receipt_json, '$.schema') =
|
||||
'qinglong/plugin-package-lifecycle-receipt@v1' AND
|
||||
json_extract(receipt_json, '$.eventDigest') = event_digest AND
|
||||
json_extract(receipt_json, '$.action') = action AND
|
||||
json_extract(receipt_json, '$.target.projectId') = project_id AND
|
||||
json_extract(receipt_json, '$.capability.status') = capability_status AND
|
||||
json_array_length(
|
||||
json_extract(receipt_json, '$.capability.taskTransitions')
|
||||
) = task_count AND
|
||||
json_extract(
|
||||
receipt_json, '$.capability.previousActiveVectorDigest'
|
||||
) = previous_active_vector_digest AND
|
||||
json_extract(
|
||||
receipt_json, '$.capability.currentActiveVectorDigest'
|
||||
) = current_active_vector_digest AND
|
||||
json_extract(receipt_json, '$.capability.currentToolSnapshotDigest') =
|
||||
current_tool_snapshot_digest AND
|
||||
json_extract(receipt_json, '$.capability.retainedSourceCount') =
|
||||
retained_source_count AND
|
||||
json_extract(receipt_json, '$.committedAtMs') = committed_at_ms AND
|
||||
json_extract(receipt_json, '$.receiptDigest') = receipt_digest
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_lifecycle_receipt_uidx ON "QingLong3PluginPackageLifecycleReceipts" (receipt_digest)`,
|
||||
`CREATE INDEX ql3_plugin_package_lifecycle_receipt_snapshot_idx ON "QingLong3PluginPackageLifecycleReceipts" (project_id, current_active_vector_digest, event_digest)`,
|
||||
`
|
||||
CREATE TABLE "QingLong3PluginPackageLifecycleTasks" (
|
||||
event_digest TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
task_id TEXT NOT NULL,
|
||||
previous_revision INTEGER NOT NULL,
|
||||
current_revision INTEGER NOT NULL,
|
||||
previous_content_digest TEXT NOT NULL,
|
||||
current_content_digest TEXT NOT NULL,
|
||||
previous_enabled INTEGER NOT NULL,
|
||||
current_enabled INTEGER NOT NULL,
|
||||
PRIMARY KEY (event_digest, task_id),
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_task_receipt_fk
|
||||
FOREIGN KEY (event_digest)
|
||||
REFERENCES "QingLong3PluginPackageLifecycleReceipts" (event_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_task_previous_fk
|
||||
FOREIGN KEY (project_id, task_id, previous_revision)
|
||||
REFERENCES "QingLong3TaskDefinitionRevisions" (
|
||||
project_id, task_id, revision
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_task_current_fk
|
||||
FOREIGN KEY (project_id, task_id, current_revision)
|
||||
REFERENCES "QingLong3TaskDefinitionRevisions" (
|
||||
project_id, task_id, revision
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_task_transition_check CHECK (
|
||||
current_revision = previous_revision + 1 AND
|
||||
previous_enabled IN (0, 1) AND current_enabled IN (0, 1) AND
|
||||
previous_enabled <> current_enabled
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_task_digest_check CHECK (
|
||||
length(previous_content_digest) = 64 AND
|
||||
previous_content_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(current_content_digest) = 64 AND
|
||||
current_content_digest NOT GLOB '*[^0-9a-f]*'
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE INDEX ql3_plugin_package_lifecycle_task_idx ON "QingLong3PluginPackageLifecycleTasks" (project_id, task_id, event_digest)`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V38 =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_project_administration":1,"local_security_audit":1,"local_security_audit_compaction":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_identity_credential_administration":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"plugin_package_quarantine":1}';
|
||||
const CAPABILITIES_V39 =
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_project_administration":1,"local_security_audit":1,"local_security_audit_compaction":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_identity_credential_administration":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"plugin_package_quarantine":1,"plugin_package_lifecycle":1}';
|
||||
|
||||
export const local0078CapabilityV39Migration = defineLocalSqliteMigration({
|
||||
id: '0078-capability-v39',
|
||||
statements: [
|
||||
`
|
||||
UPDATE "QingLong3SchemaCapabilities"
|
||||
SET contract_version = 39,
|
||||
migration_id = '0077-plugin-package-lifecycle',
|
||||
capabilities = '${CAPABILITIES_V39}',
|
||||
updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER)
|
||||
WHERE contract_name = 'local-control-core'
|
||||
AND contract_version = 38
|
||||
AND migration_id = '0075-security-audit-compactions'
|
||||
AND capabilities = '${CAPABILITIES_V38}'
|
||||
`,
|
||||
],
|
||||
});
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0079PluginPackageAutomationPublicationsMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0079-plugin-package-automation-publications',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "QingLong3PluginPackageAutomationPublications" (
|
||||
publication_digest TEXT PRIMARY KEY NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
package_name TEXT NOT NULL,
|
||||
installation_id TEXT NOT NULL,
|
||||
lock_digest TEXT NOT NULL,
|
||||
generation INTEGER NOT NULL,
|
||||
generation_digest TEXT NOT NULL,
|
||||
materialized_revision_digest TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
version INTEGER NOT NULL,
|
||||
previous_publication_digest TEXT,
|
||||
lifecycle_event_digest TEXT,
|
||||
published_at_ms INTEGER NOT NULL,
|
||||
publication_json TEXT NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_automation_publication_revision_fk
|
||||
FOREIGN KEY (generation_digest)
|
||||
REFERENCES "QingLong3PluginPackageMaterializedRevisions" (
|
||||
generation_digest
|
||||
) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_automation_publication_previous_fk
|
||||
FOREIGN KEY (previous_publication_digest)
|
||||
REFERENCES "QingLong3PluginPackageAutomationPublications" (
|
||||
publication_digest
|
||||
) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_automation_publication_lifecycle_fk
|
||||
FOREIGN KEY (lifecycle_event_digest)
|
||||
REFERENCES "QingLong3PluginPackageLifecycleEvents" (event_digest)
|
||||
ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_automation_publication_identity_check CHECK (
|
||||
length(project_id) BETWEEN 1 AND 128 AND
|
||||
length(package_name) BETWEEN 1 AND 63 AND
|
||||
length(installation_id) BETWEEN 1 AND 128 AND
|
||||
generation BETWEEN 1 AND 2147483647 AND
|
||||
state IN ('active','withdrawn','absent') AND
|
||||
version BETWEEN 1 AND 2147483647 AND
|
||||
published_at_ms >= 0 AND
|
||||
(
|
||||
version = 1 AND state IN ('active','absent') AND
|
||||
previous_publication_digest IS NULL AND
|
||||
lifecycle_event_digest IS NULL
|
||||
OR
|
||||
version > 1 AND previous_publication_digest IS NOT NULL
|
||||
) AND
|
||||
(state <> 'withdrawn' OR lifecycle_event_digest IS NOT NULL)
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_automation_publication_digest_check CHECK (
|
||||
length(publication_digest) = 64 AND
|
||||
publication_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(lock_digest) = 64 AND
|
||||
lock_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(generation_digest) = 64 AND
|
||||
generation_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(materialized_revision_digest) = 64 AND
|
||||
materialized_revision_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
(
|
||||
previous_publication_digest IS NULL OR
|
||||
length(previous_publication_digest) = 64 AND
|
||||
previous_publication_digest NOT GLOB '*[^0-9a-f]*'
|
||||
) AND
|
||||
(
|
||||
lifecycle_event_digest IS NULL OR
|
||||
length(lifecycle_event_digest) = 64 AND
|
||||
lifecycle_event_digest NOT GLOB '*[^0-9a-f]*'
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_automation_publication_json_check CHECK (
|
||||
length(CAST(publication_json AS BLOB)) BETWEEN 2 AND 12582912 AND
|
||||
json_valid(publication_json) AND
|
||||
json_type(publication_json) = 'object' AND
|
||||
json_extract(publication_json, '$.schema') =
|
||||
'qinglong/plugin-package-automation-publication@v1' AND
|
||||
json_extract(publication_json, '$.target.projectId') = project_id AND
|
||||
json_extract(publication_json, '$.target.packageName') = package_name AND
|
||||
json_extract(publication_json, '$.target.installationId') =
|
||||
installation_id AND
|
||||
json_extract(publication_json, '$.target.lockDigest') = lock_digest AND
|
||||
json_extract(publication_json, '$.target.generation') = generation AND
|
||||
json_extract(publication_json, '$.target.generationDigest') =
|
||||
generation_digest AND
|
||||
json_extract(publication_json, '$.target.materializedRevisionDigest') =
|
||||
materialized_revision_digest AND
|
||||
json_extract(publication_json, '$.state') = state AND
|
||||
json_extract(publication_json, '$.version') = version AND
|
||||
(
|
||||
previous_publication_digest IS NULL AND
|
||||
json_type(publication_json, '$.previousPublicationDigest') = 'null'
|
||||
OR
|
||||
json_extract(publication_json, '$.previousPublicationDigest') =
|
||||
previous_publication_digest
|
||||
) AND
|
||||
(
|
||||
lifecycle_event_digest IS NULL AND
|
||||
json_type(publication_json, '$.lifecycleEventDigest') = 'null'
|
||||
OR
|
||||
json_extract(publication_json, '$.lifecycleEventDigest') =
|
||||
lifecycle_event_digest
|
||||
) AND
|
||||
json_extract(publication_json, '$.publishedAtMs') = published_at_ms AND
|
||||
json_extract(publication_json, '$.publicationDigest') =
|
||||
publication_digest AND
|
||||
json_type(publication_json, '$.definitions.workflows') = 'array' AND
|
||||
json_type(publication_json, '$.definitions.prompts') = 'array' AND
|
||||
(
|
||||
state = 'absent' AND
|
||||
json_array_length(
|
||||
json_extract(publication_json, '$.definitions.workflows')
|
||||
) + json_array_length(
|
||||
json_extract(publication_json, '$.definitions.prompts')
|
||||
) = 0
|
||||
OR
|
||||
state <> 'absent' AND
|
||||
json_array_length(
|
||||
json_extract(publication_json, '$.definitions.workflows')
|
||||
) + json_array_length(
|
||||
json_extract(publication_json, '$.definitions.prompts')
|
||||
) > 0
|
||||
)
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_automation_publication_version_uidx ON "QingLong3PluginPackageAutomationPublications" (project_id, package_name, version)`,
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_automation_publication_previous_uidx ON "QingLong3PluginPackageAutomationPublications" (previous_publication_digest) WHERE previous_publication_digest IS NOT NULL`,
|
||||
`CREATE INDEX ql3_plugin_package_automation_publication_generation_idx ON "QingLong3PluginPackageAutomationPublications" (generation_digest, publication_digest)`,
|
||||
`
|
||||
CREATE TABLE "QingLong3PluginPackageAutomationPublicationHeads" (
|
||||
project_id TEXT NOT NULL,
|
||||
package_name TEXT NOT NULL,
|
||||
publication_digest TEXT NOT NULL,
|
||||
generation_digest TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
version INTEGER NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
PRIMARY KEY (project_id, package_name),
|
||||
CONSTRAINT ql3_plugin_package_automation_publication_head_publication_fk
|
||||
FOREIGN KEY (publication_digest)
|
||||
REFERENCES "QingLong3PluginPackageAutomationPublications" (
|
||||
publication_digest
|
||||
) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_automation_publication_head_state_check CHECK (
|
||||
length(project_id) BETWEEN 1 AND 128 AND
|
||||
length(package_name) BETWEEN 1 AND 63 AND
|
||||
length(publication_digest) = 64 AND
|
||||
publication_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
length(generation_digest) = 64 AND
|
||||
generation_digest NOT GLOB '*[^0-9a-f]*' AND
|
||||
state IN ('active','withdrawn','absent') AND
|
||||
version BETWEEN 1 AND 2147483647 AND
|
||||
updated_at_ms >= 0
|
||||
)
|
||||
)
|
||||
`,
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_automation_publication_head_digest_uidx ON "QingLong3PluginPackageAutomationPublicationHeads" (publication_digest)`,
|
||||
],
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user