mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
+727
@@ -0,0 +1,727 @@
|
||||
import type { PostgresClient, PostgresPool } from '@qinglong/runtime-core';
|
||||
import {
|
||||
InvalidProjectToolDefinitionSnapshotError,
|
||||
MAX_PROJECT_TOOL_DEFINITION_SNAPSHOT_JSON_BYTES,
|
||||
MAX_PROJECT_TOOL_SNAPSHOT_ACTIVE_PACKAGES,
|
||||
ProjectToolDefinitionSnapshotConflictError,
|
||||
ProjectToolDefinitionSnapshotUnavailableError,
|
||||
assertProjectToolDefinitionSnapshotRecoveryPageSize,
|
||||
assertProjectToolDefinitionSnapshotSourcePageSize,
|
||||
normalizeProjectToolDefinitionSnapshot,
|
||||
normalizeProjectToolDefinitionSnapshotPendingProjectCursor,
|
||||
normalizeProjectToolDefinitionSnapshotRecord,
|
||||
normalizeProjectToolDefinitionSnapshotSourceCursor,
|
||||
projectToolDefinitionActiveVectorDigest,
|
||||
type ProjectToolDefinitionSnapshot,
|
||||
type ProjectToolDefinitionSnapshotPendingProjectPage,
|
||||
type ProjectToolDefinitionSnapshotRecord,
|
||||
type ProjectToolDefinitionSnapshotRepository,
|
||||
type ProjectToolDefinitionSnapshotSource,
|
||||
type ProjectToolDefinitionSnapshotSourcePage,
|
||||
type ProjectToolDefinitionSnapshotSourceRepository,
|
||||
} from '@qinglong/runtime-core/project-tool-definition-snapshot';
|
||||
import { assertProjectPolicyProjectId } from '@qinglong/runtime-core/project-policy';
|
||||
|
||||
import {
|
||||
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
|
||||
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
|
||||
configurePostgresDefinitionTransaction,
|
||||
postgresRequiredInteger,
|
||||
postgresRequiredJsonObject,
|
||||
postgresRequiredString,
|
||||
postgresSqlState,
|
||||
rollbackPostgresDefinitionTransaction,
|
||||
} from '../repository/definitionRepositorySupport';
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
type Queryable = Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>;
|
||||
|
||||
function invalid(message: string): never {
|
||||
throw new InvalidProjectToolDefinitionSnapshotError(message);
|
||||
}
|
||||
|
||||
function unavailable(): ProjectToolDefinitionSnapshotUnavailableError {
|
||||
return new ProjectToolDefinitionSnapshotUnavailableError();
|
||||
}
|
||||
|
||||
function normalizeProjectId(value: unknown): string {
|
||||
try {
|
||||
assertProjectPolicyProjectId(value as string);
|
||||
} catch {
|
||||
return invalid('projectId is invalid');
|
||||
}
|
||||
return value as string;
|
||||
}
|
||||
|
||||
function serialize(snapshot: Readonly<ProjectToolDefinitionSnapshot>): string {
|
||||
const value = JSON.stringify(snapshot);
|
||||
if (
|
||||
Buffer.byteLength(value, 'utf8') >
|
||||
MAX_PROJECT_TOOL_DEFINITION_SNAPSHOT_JSON_BYTES
|
||||
) {
|
||||
return invalid('snapshot exceeds the durable JSON budget');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function same(
|
||||
left: readonly Readonly<ProjectToolDefinitionSnapshotSource>[],
|
||||
right: readonly Readonly<ProjectToolDefinitionSnapshotSource>[],
|
||||
): boolean {
|
||||
return JSON.stringify(left) === JSON.stringify(right);
|
||||
}
|
||||
|
||||
function exactOptions(
|
||||
value: unknown,
|
||||
required: readonly string[],
|
||||
optional: readonly string[],
|
||||
label: string,
|
||||
): asserts value is Record<string, unknown> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return invalid(`${label} is invalid`);
|
||||
}
|
||||
const keys = Reflect.ownKeys(value);
|
||||
const allowed = new Set([...required, ...optional]);
|
||||
if (
|
||||
required.some((key) => !keys.includes(key)) ||
|
||||
keys.some((key) => typeof key !== 'string' || !allowed.has(key))
|
||||
) {
|
||||
invalid(`${label} shape is invalid`);
|
||||
}
|
||||
}
|
||||
|
||||
function sourceFromRow(
|
||||
row: Row,
|
||||
): Readonly<ProjectToolDefinitionSnapshotSource> {
|
||||
if (postgresRequiredString(row.activeState, unavailable) !== 'active') {
|
||||
throw unavailable();
|
||||
}
|
||||
return Object.freeze({
|
||||
installationId: postgresRequiredString(row.installationId, unavailable),
|
||||
packageName: postgresRequiredString(row.packageName, unavailable),
|
||||
generation: postgresRequiredInteger(row.generation, unavailable),
|
||||
generationDigest: postgresRequiredString(row.generationDigest, unavailable),
|
||||
lockDigest: postgresRequiredString(row.lockDigest, unavailable),
|
||||
revisionDigest: postgresRequiredString(row.revisionDigest, unavailable),
|
||||
});
|
||||
}
|
||||
|
||||
function mappedError(error: unknown): Error {
|
||||
if (
|
||||
error instanceof InvalidProjectToolDefinitionSnapshotError ||
|
||||
error instanceof ProjectToolDefinitionSnapshotConflictError ||
|
||||
error instanceof ProjectToolDefinitionSnapshotUnavailableError
|
||||
) {
|
||||
return error;
|
||||
}
|
||||
const state = postgresSqlState(error);
|
||||
if (state === '23503' || state === '23505' || state === '23514') {
|
||||
return new ProjectToolDefinitionSnapshotConflictError(
|
||||
'snapshot identity is already bound',
|
||||
);
|
||||
}
|
||||
return new ProjectToolDefinitionSnapshotUnavailableError({
|
||||
cause: error instanceof Error ? error : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export class PostgresProjectToolDefinitionSnapshotRepository
|
||||
implements
|
||||
ProjectToolDefinitionSnapshotRepository,
|
||||
ProjectToolDefinitionSnapshotSourceRepository
|
||||
{
|
||||
constructor(private readonly pool: PostgresPool) {
|
||||
if (
|
||||
!pool ||
|
||||
typeof pool.query !== 'function' ||
|
||||
typeof pool.connect !== 'function'
|
||||
) {
|
||||
throw new TypeError(
|
||||
'PostgreSQL Project Tool Definition snapshot repository options are invalid',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async currentSources(
|
||||
queryable: Queryable,
|
||||
projectId: string,
|
||||
lock: boolean,
|
||||
): Promise<readonly Readonly<ProjectToolDefinitionSnapshotSource>[]> {
|
||||
const result = await queryable.query<Row>(
|
||||
`SELECT
|
||||
head.package_name AS "packageName",
|
||||
active_install.installation_id AS "installationId",
|
||||
active_install.target_generation AS "generation",
|
||||
revision.generation_digest AS "generationDigest",
|
||||
active_install.lock_digest AS "lockDigest",
|
||||
revision.revision_digest AS "revisionDigest",
|
||||
active_install.state AS "activeState"
|
||||
FROM "ql3"."plugin_package_install_heads" AS head
|
||||
JOIN "ql3"."plugin_package_installs" AS head_install
|
||||
ON head_install.installation_id = head.installation_id
|
||||
LEFT JOIN "ql3"."plugin_package_installs" AS active_install
|
||||
ON active_install.project_id = head.project_id
|
||||
AND active_install.package_name = head.package_name
|
||||
AND active_install.lock_digest = head_install.active_lock_digest
|
||||
LEFT JOIN "ql3"."plugin_package_materialized_revisions" AS revision
|
||||
ON revision.project_id = active_install.project_id
|
||||
AND revision.package_name = active_install.package_name
|
||||
AND revision.generation = active_install.target_generation
|
||||
AND revision.lock_digest = active_install.lock_digest
|
||||
LEFT JOIN "ql3"."plugin_package_quarantine_events" AS quarantine
|
||||
ON quarantine.project_id = active_install.project_id
|
||||
AND quarantine.package_name = active_install.package_name
|
||||
AND quarantine.installation_id = active_install.installation_id
|
||||
AND quarantine.lock_digest = active_install.lock_digest
|
||||
LEFT JOIN "ql3"."plugin_package_lifecycle_heads" AS lifecycle
|
||||
ON lifecycle.project_id = active_install.project_id
|
||||
AND lifecycle.package_name = active_install.package_name
|
||||
AND lifecycle.installation_id = active_install.installation_id
|
||||
AND lifecycle.lock_digest = active_install.lock_digest
|
||||
AND lifecycle.install_record_digest = active_install.record_digest
|
||||
WHERE head.project_id = $1
|
||||
AND head_install.active_lock_digest IS NOT NULL
|
||||
AND quarantine.event_digest IS NULL
|
||||
AND (
|
||||
lifecycle.event_digest IS NULL OR lifecycle.disposition = 'active'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."plugin_package_publisher_provenance" AS provenance
|
||||
JOIN "ql3"."plugin_package_publisher_revocation_receipts" AS revoked
|
||||
ON revoked.publisher = provenance.publisher
|
||||
AND revoked.key_id = provenance.key_id
|
||||
WHERE provenance.installation_id = active_install.installation_id
|
||||
AND provenance.lock_digest = active_install.lock_digest
|
||||
)
|
||||
ORDER BY head.package_name, active_install.installation_id
|
||||
${lock ? 'FOR SHARE OF head, head_install' : ''}`,
|
||||
[projectId],
|
||||
);
|
||||
if (result.rows.length > MAX_PROJECT_TOOL_SNAPSHOT_ACTIVE_PACKAGES) {
|
||||
throw unavailable();
|
||||
}
|
||||
const sources = result.rows.map(sourceFromRow);
|
||||
if (
|
||||
sources.some(
|
||||
(source, index) =>
|
||||
index > 0 && sources[index - 1]!.packageName >= source.packageName,
|
||||
)
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
return Object.freeze(sources);
|
||||
}
|
||||
|
||||
private activeVectorDigest(
|
||||
projectId: string,
|
||||
sources: readonly Readonly<ProjectToolDefinitionSnapshotSource>[],
|
||||
): string {
|
||||
try {
|
||||
return projectToolDefinitionActiveVectorDigest(projectId, sources);
|
||||
} catch {
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
private async sourceRows(
|
||||
queryable: Queryable,
|
||||
projectId: string,
|
||||
activeVectorDigest: string,
|
||||
): Promise<readonly Readonly<ProjectToolDefinitionSnapshotSource>[]> {
|
||||
const result = await queryable.query<Row>(
|
||||
`SELECT
|
||||
installation_id AS "installationId",
|
||||
package_name AS "packageName",
|
||||
generation,
|
||||
generation_digest AS "generationDigest",
|
||||
lock_digest AS "lockDigest",
|
||||
revision_digest AS "revisionDigest"
|
||||
FROM "ql3"."project_tool_definition_snapshot_sources"
|
||||
WHERE project_id = $1 AND active_vector_digest = $2
|
||||
ORDER BY package_name`,
|
||||
[projectId, activeVectorDigest],
|
||||
);
|
||||
return Object.freeze(
|
||||
result.rows.map((row) =>
|
||||
Object.freeze({
|
||||
installationId: postgresRequiredString(
|
||||
row.installationId,
|
||||
unavailable,
|
||||
),
|
||||
packageName: postgresRequiredString(row.packageName, unavailable),
|
||||
generation: postgresRequiredInteger(row.generation, unavailable),
|
||||
generationDigest: postgresRequiredString(
|
||||
row.generationDigest,
|
||||
unavailable,
|
||||
),
|
||||
lockDigest: postgresRequiredString(row.lockDigest, unavailable),
|
||||
revisionDigest: postgresRequiredString(
|
||||
row.revisionDigest,
|
||||
unavailable,
|
||||
),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private async findStored(
|
||||
queryable: Queryable,
|
||||
projectId: string,
|
||||
activeVectorDigest: string,
|
||||
): Promise<Readonly<ProjectToolDefinitionSnapshotRecord> | null> {
|
||||
const result = await queryable.query<Row>(
|
||||
`SELECT
|
||||
project_id AS "projectId",
|
||||
active_vector_digest AS "activeVectorDigest",
|
||||
definitions_digest AS "definitionsDigest",
|
||||
snapshot_digest AS "snapshotDigest",
|
||||
snapshot_json AS "snapshotJson",
|
||||
committed_at_ms AS "committedAtMs"
|
||||
FROM "ql3"."project_tool_definition_snapshots"
|
||||
WHERE project_id = $1 AND active_vector_digest = $2
|
||||
LIMIT 2`,
|
||||
[projectId, activeVectorDigest],
|
||||
);
|
||||
if (result.rows.length === 0) return null;
|
||||
if (result.rows.length !== 1) throw unavailable();
|
||||
const row = result.rows[0]!;
|
||||
try {
|
||||
const snapshot = normalizeProjectToolDefinitionSnapshot(
|
||||
postgresRequiredJsonObject(
|
||||
row.snapshotJson,
|
||||
unavailable,
|
||||
) as unknown as ProjectToolDefinitionSnapshot,
|
||||
);
|
||||
const record = normalizeProjectToolDefinitionSnapshotRecord({
|
||||
snapshot,
|
||||
committedAtMs: postgresRequiredInteger(row.committedAtMs, unavailable),
|
||||
});
|
||||
if (
|
||||
snapshot.projectId !==
|
||||
postgresRequiredString(row.projectId, unavailable) ||
|
||||
snapshot.activeVectorDigest !==
|
||||
postgresRequiredString(row.activeVectorDigest, unavailable) ||
|
||||
snapshot.definitionsDigest !==
|
||||
postgresRequiredString(row.definitionsDigest, unavailable) ||
|
||||
snapshot.snapshotDigest !==
|
||||
postgresRequiredString(row.snapshotDigest, unavailable) ||
|
||||
!same(
|
||||
snapshot.sources,
|
||||
await this.sourceRows(
|
||||
queryable,
|
||||
snapshot.projectId,
|
||||
snapshot.activeVectorDigest,
|
||||
),
|
||||
)
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
return record;
|
||||
} catch (error) {
|
||||
if (error instanceof ProjectToolDefinitionSnapshotUnavailableError) {
|
||||
throw error;
|
||||
}
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
async findCurrent(
|
||||
projectIdValue: string,
|
||||
): Promise<Readonly<ProjectToolDefinitionSnapshotRecord> | null> {
|
||||
const projectId = normalizeProjectId(projectIdValue);
|
||||
let client: PostgresClient;
|
||||
try {
|
||||
client = await this.pool.connect();
|
||||
} catch {
|
||||
throw unavailable();
|
||||
}
|
||||
let began = false;
|
||||
try {
|
||||
await client.query('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
||||
began = true;
|
||||
await client.query(`SELECT set_config('statement_timeout', $1, true)`, [
|
||||
'5000ms',
|
||||
]);
|
||||
const sources = await this.currentSources(client, projectId, false);
|
||||
const record = await this.findStored(
|
||||
client,
|
||||
projectId,
|
||||
this.activeVectorDigest(projectId, sources),
|
||||
);
|
||||
if (record && !same(record.snapshot.sources, sources)) {
|
||||
throw unavailable();
|
||||
}
|
||||
await client.query('COMMIT');
|
||||
began = false;
|
||||
return record;
|
||||
} catch (error) {
|
||||
if (began) await rollbackPostgresDefinitionTransaction(client);
|
||||
throw mappedError(error);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async listActiveSourcePage(options: {
|
||||
readonly projectId: string;
|
||||
readonly limit: number;
|
||||
readonly after?: Readonly<{ readonly packageName: string }>;
|
||||
}): Promise<Readonly<ProjectToolDefinitionSnapshotSourcePage>> {
|
||||
exactOptions(
|
||||
options,
|
||||
['limit', 'projectId'],
|
||||
['after'],
|
||||
'snapshot source page options',
|
||||
);
|
||||
const projectId = normalizeProjectId(options.projectId);
|
||||
assertProjectToolDefinitionSnapshotSourcePageSize(options.limit);
|
||||
const after =
|
||||
options.after === undefined
|
||||
? undefined
|
||||
: normalizeProjectToolDefinitionSnapshotSourceCursor(options.after);
|
||||
try {
|
||||
const result = await this.pool.query<Row>(
|
||||
`SELECT
|
||||
head.package_name AS "packageName",
|
||||
active_install.installation_id AS "installationId",
|
||||
active_install.target_generation AS "generation",
|
||||
revision.generation_digest AS "generationDigest",
|
||||
active_install.lock_digest AS "lockDigest",
|
||||
revision.revision_digest AS "revisionDigest",
|
||||
active_install.state AS "activeState"
|
||||
FROM "ql3"."plugin_package_install_heads" AS head
|
||||
JOIN "ql3"."plugin_package_installs" AS head_install
|
||||
ON head_install.installation_id = head.installation_id
|
||||
LEFT JOIN "ql3"."plugin_package_installs" AS active_install
|
||||
ON active_install.project_id = head.project_id
|
||||
AND active_install.package_name = head.package_name
|
||||
AND active_install.lock_digest = head_install.active_lock_digest
|
||||
LEFT JOIN "ql3"."plugin_package_materialized_revisions" AS revision
|
||||
ON revision.project_id = active_install.project_id
|
||||
AND revision.package_name = active_install.package_name
|
||||
AND revision.generation = active_install.target_generation
|
||||
AND revision.lock_digest = active_install.lock_digest
|
||||
LEFT JOIN "ql3"."plugin_package_quarantine_events" AS quarantine
|
||||
ON quarantine.project_id = active_install.project_id
|
||||
AND quarantine.package_name = active_install.package_name
|
||||
AND quarantine.installation_id = active_install.installation_id
|
||||
AND quarantine.lock_digest = active_install.lock_digest
|
||||
WHERE head.project_id = $1
|
||||
AND head_install.active_lock_digest IS NOT NULL
|
||||
AND quarantine.event_digest IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."plugin_package_publisher_provenance" AS provenance
|
||||
JOIN "ql3"."plugin_package_publisher_revocation_receipts" AS revoked
|
||||
ON revoked.publisher = provenance.publisher
|
||||
AND revoked.key_id = provenance.key_id
|
||||
WHERE provenance.installation_id =
|
||||
active_install.installation_id
|
||||
AND provenance.lock_digest = active_install.lock_digest
|
||||
)
|
||||
AND head.package_name > $2
|
||||
ORDER BY head.package_name, active_install.installation_id
|
||||
LIMIT $3`,
|
||||
[projectId, after?.packageName ?? '', options.limit + 1],
|
||||
);
|
||||
const truncated = result.rows.length > options.limit;
|
||||
const sources = Object.freeze(
|
||||
result.rows.slice(0, options.limit).map(sourceFromRow),
|
||||
);
|
||||
if (
|
||||
sources.some(
|
||||
(source, index) =>
|
||||
index > 0 && sources[index - 1]!.packageName >= source.packageName,
|
||||
)
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
const last = sources.at(-1);
|
||||
return Object.freeze({
|
||||
sources,
|
||||
truncated,
|
||||
...(truncated && last
|
||||
? { next: Object.freeze({ packageName: last.packageName }) }
|
||||
: {}),
|
||||
});
|
||||
} catch (error) {
|
||||
throw mappedError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async listPendingProjectPage(options: {
|
||||
readonly limit: number;
|
||||
readonly after?: Readonly<{ readonly projectId: string }>;
|
||||
}): Promise<Readonly<ProjectToolDefinitionSnapshotPendingProjectPage>> {
|
||||
exactOptions(
|
||||
options,
|
||||
['limit'],
|
||||
['after'],
|
||||
'snapshot pending Project page options',
|
||||
);
|
||||
assertProjectToolDefinitionSnapshotRecoveryPageSize(options.limit);
|
||||
const after =
|
||||
options.after === undefined
|
||||
? undefined
|
||||
: normalizeProjectToolDefinitionSnapshotPendingProjectCursor(
|
||||
options.after,
|
||||
);
|
||||
try {
|
||||
const result = await this.pool.query<Row>(
|
||||
`WITH active_sources AS (
|
||||
SELECT
|
||||
head.project_id,
|
||||
head.package_name,
|
||||
active_install.installation_id,
|
||||
active_install.target_generation AS generation,
|
||||
revision.generation_digest,
|
||||
active_install.lock_digest,
|
||||
revision.revision_digest,
|
||||
CASE
|
||||
WHEN active_install.state = 'active'
|
||||
AND active_install.installation_id IS NOT NULL
|
||||
AND revision.generation_digest IS NOT NULL
|
||||
AND revision.revision_digest IS NOT NULL
|
||||
THEN true ELSE false
|
||||
END AS valid
|
||||
FROM "ql3"."plugin_package_install_heads" AS head
|
||||
JOIN "ql3"."plugin_package_installs" AS head_install
|
||||
ON head_install.installation_id = head.installation_id
|
||||
LEFT JOIN "ql3"."plugin_package_installs" AS active_install
|
||||
ON active_install.project_id = head.project_id
|
||||
AND active_install.package_name = head.package_name
|
||||
AND active_install.lock_digest = head_install.active_lock_digest
|
||||
LEFT JOIN "ql3"."plugin_package_materialized_revisions" AS revision
|
||||
ON revision.project_id = active_install.project_id
|
||||
AND revision.package_name = active_install.package_name
|
||||
AND revision.generation = active_install.target_generation
|
||||
AND revision.lock_digest = active_install.lock_digest
|
||||
LEFT JOIN "ql3"."plugin_package_quarantine_events" AS quarantine
|
||||
ON quarantine.project_id = active_install.project_id
|
||||
AND quarantine.package_name = active_install.package_name
|
||||
AND quarantine.installation_id = active_install.installation_id
|
||||
AND quarantine.lock_digest = active_install.lock_digest
|
||||
WHERE head_install.active_lock_digest IS NOT NULL
|
||||
AND quarantine.event_digest IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."plugin_package_publisher_provenance" AS provenance
|
||||
JOIN "ql3"."plugin_package_publisher_revocation_receipts" AS revoked
|
||||
ON revoked.publisher = provenance.publisher
|
||||
AND revoked.key_id = provenance.key_id
|
||||
WHERE provenance.installation_id =
|
||||
active_install.installation_id
|
||||
AND provenance.lock_digest = active_install.lock_digest
|
||||
)
|
||||
)
|
||||
SELECT project.id AS "projectId"
|
||||
FROM "ql3"."projects" AS project
|
||||
WHERE project.status = 'active'
|
||||
AND project.id COLLATE "C" > $1 COLLATE "C"
|
||||
AND (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM active_sources AS active
|
||||
WHERE active.project_id = project.id
|
||||
AND active.valid = false
|
||||
)
|
||||
OR NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."project_tool_definition_snapshots" AS snapshot
|
||||
WHERE snapshot.project_id = project.id
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM active_sources AS active
|
||||
WHERE active.project_id = project.id
|
||||
AND active.valid = true
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."project_tool_definition_snapshot_sources" AS stored
|
||||
WHERE stored.project_id = snapshot.project_id
|
||||
AND stored.active_vector_digest =
|
||||
snapshot.active_vector_digest
|
||||
AND stored.package_name = active.package_name
|
||||
AND stored.installation_id = active.installation_id
|
||||
AND stored.generation = active.generation
|
||||
AND stored.generation_digest =
|
||||
active.generation_digest
|
||||
AND stored.lock_digest = active.lock_digest
|
||||
AND stored.revision_digest =
|
||||
active.revision_digest
|
||||
)
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."project_tool_definition_snapshot_sources" AS stored
|
||||
WHERE stored.project_id = snapshot.project_id
|
||||
AND stored.active_vector_digest =
|
||||
snapshot.active_vector_digest
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM active_sources AS active
|
||||
WHERE active.project_id = project.id
|
||||
AND active.valid = true
|
||||
AND active.package_name = stored.package_name
|
||||
AND active.installation_id =
|
||||
stored.installation_id
|
||||
AND active.generation = stored.generation
|
||||
AND active.generation_digest =
|
||||
stored.generation_digest
|
||||
AND active.lock_digest = stored.lock_digest
|
||||
AND active.revision_digest =
|
||||
stored.revision_digest
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
ORDER BY project.id COLLATE "C"
|
||||
LIMIT $2`,
|
||||
[after?.projectId ?? '', options.limit + 1],
|
||||
);
|
||||
const truncated = result.rows.length > options.limit;
|
||||
const projectIds = Object.freeze(
|
||||
result.rows
|
||||
.slice(0, options.limit)
|
||||
.map((row) =>
|
||||
normalizeProjectId(
|
||||
postgresRequiredString(row.projectId, unavailable),
|
||||
),
|
||||
),
|
||||
);
|
||||
const last = projectIds.at(-1);
|
||||
return Object.freeze({
|
||||
projectIds,
|
||||
truncated,
|
||||
...(truncated && last
|
||||
? { next: Object.freeze({ projectId: last }) }
|
||||
: {}),
|
||||
});
|
||||
} catch (error) {
|
||||
throw mappedError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async publish(value: Readonly<ProjectToolDefinitionSnapshot>): Promise<
|
||||
Readonly<{
|
||||
status: 'created' | 'existing';
|
||||
record: Readonly<ProjectToolDefinitionSnapshotRecord>;
|
||||
}>
|
||||
> {
|
||||
const snapshot = normalizeProjectToolDefinitionSnapshot(value);
|
||||
const snapshotJson = serialize(snapshot);
|
||||
for (
|
||||
let attempt = 0;
|
||||
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
|
||||
attempt += 1
|
||||
) {
|
||||
let client: PostgresClient;
|
||||
try {
|
||||
client = await this.pool.connect();
|
||||
} catch {
|
||||
throw unavailable();
|
||||
}
|
||||
let began = false;
|
||||
try {
|
||||
await configurePostgresDefinitionTransaction(client);
|
||||
began = true;
|
||||
const sources = await this.currentSources(
|
||||
client,
|
||||
snapshot.projectId,
|
||||
true,
|
||||
);
|
||||
if (
|
||||
!same(snapshot.sources, sources) ||
|
||||
this.activeVectorDigest(snapshot.projectId, sources) !==
|
||||
snapshot.activeVectorDigest
|
||||
) {
|
||||
throw new ProjectToolDefinitionSnapshotConflictError(
|
||||
'snapshot source vector is not the current active Package vector',
|
||||
);
|
||||
}
|
||||
const inserted = await client.query(
|
||||
`INSERT INTO "ql3"."project_tool_definition_snapshots" (
|
||||
project_id, active_vector_digest, definitions_digest,
|
||||
snapshot_digest, snapshot_json, committed_at_ms
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5::jsonb,
|
||||
floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint)
|
||||
ON CONFLICT (project_id, active_vector_digest) DO NOTHING
|
||||
RETURNING active_vector_digest`,
|
||||
[
|
||||
snapshot.projectId,
|
||||
snapshot.activeVectorDigest,
|
||||
snapshot.definitionsDigest,
|
||||
snapshot.snapshotDigest,
|
||||
snapshotJson,
|
||||
],
|
||||
);
|
||||
if (inserted.rows.length === 1 && snapshot.sources.length > 0) {
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."project_tool_definition_snapshot_sources" (
|
||||
project_id, active_vector_digest, package_name,
|
||||
installation_id, generation, generation_digest,
|
||||
lock_digest, revision_digest
|
||||
)
|
||||
SELECT $1, $2, source.package_name, source.installation_id,
|
||||
source.generation, source.generation_digest,
|
||||
source.lock_digest, source.revision_digest
|
||||
FROM jsonb_to_recordset($3::jsonb) AS source(
|
||||
package_name varchar(63),
|
||||
installation_id varchar(128),
|
||||
generation integer,
|
||||
generation_digest char(64),
|
||||
lock_digest char(64),
|
||||
revision_digest char(64)
|
||||
)`,
|
||||
[
|
||||
snapshot.projectId,
|
||||
snapshot.activeVectorDigest,
|
||||
JSON.stringify(
|
||||
snapshot.sources.map((source) => ({
|
||||
package_name: source.packageName,
|
||||
installation_id: source.installationId,
|
||||
generation: source.generation,
|
||||
generation_digest: source.generationDigest,
|
||||
lock_digest: source.lockDigest,
|
||||
revision_digest: source.revisionDigest,
|
||||
})),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
const stored = await this.findStored(
|
||||
client,
|
||||
snapshot.projectId,
|
||||
snapshot.activeVectorDigest,
|
||||
);
|
||||
if (!stored) throw unavailable();
|
||||
if (JSON.stringify(stored.snapshot) !== snapshotJson) {
|
||||
throw new ProjectToolDefinitionSnapshotConflictError(
|
||||
'active vector is bound to another semantic snapshot',
|
||||
);
|
||||
}
|
||||
await client.query('COMMIT');
|
||||
began = false;
|
||||
return Object.freeze({
|
||||
status: inserted.rows.length === 1 ? 'created' : 'existing',
|
||||
record: stored,
|
||||
});
|
||||
} catch (error) {
|
||||
if (began) await rollbackPostgresDefinitionTransaction(client);
|
||||
const state = postgresSqlState(error);
|
||||
if (
|
||||
state &&
|
||||
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
|
||||
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
throw mappedError(error);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
+705
@@ -0,0 +1,705 @@
|
||||
import type {
|
||||
PostgresClient,
|
||||
PostgresPool,
|
||||
PostgresQueryable,
|
||||
} from '@qinglong/runtime-core';
|
||||
import {
|
||||
InvalidToolExecutionCompletionError,
|
||||
MAX_TOOL_EXECUTION_COMPLETION_JSON_BYTES,
|
||||
MAX_TOOL_EXECUTION_RESULT_ARTIFACT_JSON_BYTES,
|
||||
ToolExecutionCompletionConflictError,
|
||||
ToolExecutionCompletionUnavailableError,
|
||||
normalizeToolExecutionCompletionCommand,
|
||||
normalizeToolExecutionCompletionRecord,
|
||||
normalizeToolExecutionResultKeyBinding,
|
||||
normalizeToolExecutionResultArtifact,
|
||||
toolExecutionCompletionRecord,
|
||||
toolExecutionResultKeyBinding,
|
||||
type CommitToolExecutionCompletionResult,
|
||||
type ToolExecutionCompletionCommand,
|
||||
type ToolExecutionCompletionRecord,
|
||||
type ToolExecutionCompletionRepository,
|
||||
type ToolExecutionResultArtifact,
|
||||
type ToolExecutionResultKeyBinding,
|
||||
} from '@qinglong/runtime-core/tool-execution-completion';
|
||||
import {
|
||||
normalizeToolResultKeyCatalogRecord,
|
||||
requireActiveToolResultKey,
|
||||
toolResultKeyCatalogFence,
|
||||
type ToolResultKeyCatalogRecord,
|
||||
} from '@qinglong/runtime-core/tool-result-key-catalog';
|
||||
import type { StepRunMutation } from '@qinglong/runtime-core/step-run';
|
||||
import { normalizeToolExecutionStartBarrierRecord } from '@qinglong/runtime-core/tool-execution-start-barrier';
|
||||
|
||||
import {
|
||||
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
|
||||
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
|
||||
configurePostgresDefinitionTransaction,
|
||||
postgresRequiredInteger,
|
||||
postgresRequiredJsonObject,
|
||||
postgresRequiredString,
|
||||
postgresSqlState,
|
||||
rollbackPostgresDefinitionTransaction,
|
||||
} from '../repository/definitionRepositorySupport';
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
type Queryable = Pick<PostgresQueryable, 'query'>;
|
||||
|
||||
const IDENTITY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
const TERMINAL_RUN_STATUSES = new Set([
|
||||
'succeeded',
|
||||
'failed',
|
||||
'cancelled',
|
||||
'timed_out',
|
||||
]);
|
||||
const CATALOG_TRANSACTION_LOCK = 'SELECT pg_advisory_xact_lock(190397473, 3)';
|
||||
const COMPLETION_SELECT = `
|
||||
completion.completion_json AS "completionJson",
|
||||
completion.artifact_json AS "artifactJson",
|
||||
completion.start_id AS "storedStartId",
|
||||
completion.artifact_id AS "storedArtifactId",
|
||||
completion.project_id AS "storedProjectId",
|
||||
completion.run_id AS "storedRunId",
|
||||
completion.step_run_id AS "storedStepRunId",
|
||||
completion.started_step_run_version AS "storedStartedStepRunVersion",
|
||||
completion.completed_step_run_version AS "storedCompletedStepRunVersion",
|
||||
completion.barrier_digest AS "storedBarrierDigest",
|
||||
completion.adapter_digest AS "storedAdapterDigest",
|
||||
completion.output_digest AS "storedOutputDigest",
|
||||
completion.execution_result_digest AS "storedExecutionResultDigest",
|
||||
completion.artifact_digest AS "storedArtifactDigest",
|
||||
completion.key_id AS "storedKeyId",
|
||||
completion.algorithm AS "storedAlgorithm",
|
||||
completion.plaintext_bytes AS "storedPlaintextBytes",
|
||||
completion.step_run_mutation_id AS "storedMutationId",
|
||||
completion.step_run_mutation_digest AS "storedMutationDigest",
|
||||
completion.completed_step_run_digest AS "storedCompletedStepRunDigest",
|
||||
completion.run_event_id AS "storedRunEventId",
|
||||
completion.completed_at_ms AS "storedCompletedAtMs",
|
||||
completion.completion_digest AS "storedCompletionDigest",
|
||||
binding.artifact_digest AS "bindingArtifactDigest",
|
||||
binding.catalog_generation AS "bindingCatalogGeneration",
|
||||
binding.catalog_digest AS "bindingCatalogDigest",
|
||||
binding.key_id AS "bindingKeyId",
|
||||
binding.material_proof AS "bindingMaterialProof",
|
||||
binding.binding_digest AS "bindingDigest",
|
||||
catalog.catalog_json AS "catalogJson",
|
||||
barrier.barrier_digest AS "joinedBarrierDigest",
|
||||
mutation.mutation_digest AS "joinedMutationDigest",
|
||||
mutation.step_run_digest AS "joinedCompletedStepRunDigest",
|
||||
event.id AS "joinedRunEventId"
|
||||
`;
|
||||
|
||||
function unavailable(cause?: unknown): ToolExecutionCompletionUnavailableError {
|
||||
return new ToolExecutionCompletionUnavailableError({
|
||||
cause: cause instanceof Error ? cause : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function requiredText(row: Row, key: string): string {
|
||||
return postgresRequiredString(row[key], unavailable);
|
||||
}
|
||||
|
||||
function requiredInteger(row: Row, key: string): number {
|
||||
return postgresRequiredInteger(row[key], unavailable);
|
||||
}
|
||||
|
||||
function requiredJson(row: Row, key: string): Record<string, unknown> {
|
||||
return postgresRequiredJsonObject(row[key], unavailable);
|
||||
}
|
||||
|
||||
function identity(value: unknown, label: string): string {
|
||||
if (typeof value !== 'string' || !IDENTITY_PATTERN.test(value)) {
|
||||
throw new InvalidToolExecutionCompletionError(`${label} is invalid`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function constraintError(error: unknown): boolean {
|
||||
const state = postgresSqlState(error);
|
||||
return state === '23503' || state === '23505' || state === '23514';
|
||||
}
|
||||
|
||||
function mapStorageError(error: unknown): Error {
|
||||
if (
|
||||
error instanceof InvalidToolExecutionCompletionError ||
|
||||
error instanceof ToolExecutionCompletionConflictError ||
|
||||
error instanceof ToolExecutionCompletionUnavailableError
|
||||
) {
|
||||
return error;
|
||||
}
|
||||
return constraintError(error)
|
||||
? new ToolExecutionCompletionConflictError()
|
||||
: unavailable(error);
|
||||
}
|
||||
|
||||
function valuesFromRow(row: Row): Readonly<{
|
||||
artifact: Readonly<ToolExecutionResultArtifact>;
|
||||
binding: Readonly<ToolExecutionResultKeyBinding>;
|
||||
completion: Readonly<ToolExecutionCompletionRecord>;
|
||||
}> {
|
||||
let completion: Readonly<ToolExecutionCompletionRecord>;
|
||||
let artifact: Readonly<ToolExecutionResultArtifact>;
|
||||
let binding: Readonly<ToolExecutionResultKeyBinding>;
|
||||
let catalog: Readonly<ToolResultKeyCatalogRecord>;
|
||||
try {
|
||||
completion = normalizeToolExecutionCompletionRecord(
|
||||
requiredJson(
|
||||
row,
|
||||
'completionJson',
|
||||
) as unknown as ToolExecutionCompletionRecord,
|
||||
);
|
||||
artifact = normalizeToolExecutionResultArtifact(
|
||||
requiredJson(
|
||||
row,
|
||||
'artifactJson',
|
||||
) as unknown as ToolExecutionResultArtifact,
|
||||
);
|
||||
binding = normalizeToolExecutionResultKeyBinding({
|
||||
schema: 'qinglong/tool-execution-result-key-binding@v1',
|
||||
startId: completion.startId,
|
||||
artifactId: artifact.artifactId,
|
||||
artifactDigest: requiredText(row, 'bindingArtifactDigest'),
|
||||
catalogGeneration: requiredInteger(row, 'bindingCatalogGeneration'),
|
||||
catalogDigest: requiredText(row, 'bindingCatalogDigest'),
|
||||
keyId: requiredText(row, 'bindingKeyId'),
|
||||
materialProof: requiredText(row, 'bindingMaterialProof'),
|
||||
bindingDigest: requiredText(row, 'bindingDigest'),
|
||||
});
|
||||
catalog = normalizeToolResultKeyCatalogRecord(
|
||||
requiredJson(row, 'catalogJson') as unknown as ToolResultKeyCatalogRecord,
|
||||
);
|
||||
} catch {
|
||||
throw unavailable();
|
||||
}
|
||||
if (
|
||||
Buffer.byteLength(JSON.stringify(completion), 'utf8') >
|
||||
MAX_TOOL_EXECUTION_COMPLETION_JSON_BYTES ||
|
||||
Buffer.byteLength(JSON.stringify(artifact), 'utf8') >
|
||||
MAX_TOOL_EXECUTION_RESULT_ARTIFACT_JSON_BYTES ||
|
||||
completion.startId !== requiredText(row, 'storedStartId') ||
|
||||
completion.resultArtifact.artifactId !==
|
||||
requiredText(row, 'storedArtifactId') ||
|
||||
completion.projectId !== requiredText(row, 'storedProjectId') ||
|
||||
completion.runId !== requiredText(row, 'storedRunId') ||
|
||||
completion.stepRunId !== requiredText(row, 'storedStepRunId') ||
|
||||
completion.startedStepRunVersion !==
|
||||
requiredInteger(row, 'storedStartedStepRunVersion') ||
|
||||
completion.completedStepRunVersion !==
|
||||
requiredInteger(row, 'storedCompletedStepRunVersion') ||
|
||||
completion.barrierDigest !== requiredText(row, 'storedBarrierDigest') ||
|
||||
completion.adapterDigest !== requiredText(row, 'storedAdapterDigest') ||
|
||||
completion.resultArtifact.outputDigest !==
|
||||
requiredText(row, 'storedOutputDigest') ||
|
||||
completion.resultArtifact.executionResultDigest !==
|
||||
requiredText(row, 'storedExecutionResultDigest') ||
|
||||
completion.resultArtifact.artifactDigest !==
|
||||
requiredText(row, 'storedArtifactDigest') ||
|
||||
completion.stepRunMutationId !== requiredText(row, 'storedMutationId') ||
|
||||
completion.stepRunMutationDigest !==
|
||||
requiredText(row, 'storedMutationDigest') ||
|
||||
completion.completedStepRunDigest !==
|
||||
requiredText(row, 'storedCompletedStepRunDigest') ||
|
||||
completion.runEventId !== requiredText(row, 'storedRunEventId') ||
|
||||
completion.completedAtMs !== requiredInteger(row, 'storedCompletedAtMs') ||
|
||||
completion.completionDigest !==
|
||||
requiredText(row, 'storedCompletionDigest') ||
|
||||
artifact.artifactId !== completion.resultArtifact.artifactId ||
|
||||
artifact.artifactDigest !== completion.resultArtifact.artifactDigest ||
|
||||
artifact.projectId !== completion.projectId ||
|
||||
artifact.startId !== completion.startId ||
|
||||
artifact.runId !== completion.runId ||
|
||||
artifact.stepRunId !== completion.stepRunId ||
|
||||
artifact.barrierDigest !== completion.barrierDigest ||
|
||||
artifact.adapterDigest !== completion.adapterDigest ||
|
||||
artifact.outputDigest !== completion.resultArtifact.outputDigest ||
|
||||
artifact.executionResultDigest !==
|
||||
completion.resultArtifact.executionResultDigest ||
|
||||
artifact.keyId !== requiredText(row, 'storedKeyId') ||
|
||||
artifact.algorithm !== requiredText(row, 'storedAlgorithm') ||
|
||||
artifact.plaintextBytes !== requiredInteger(row, 'storedPlaintextBytes') ||
|
||||
artifact.sealedAtMs !== completion.completedAtMs ||
|
||||
binding.startId !== completion.startId ||
|
||||
binding.artifactId !== artifact.artifactId ||
|
||||
binding.artifactDigest !== artifact.artifactDigest ||
|
||||
binding.catalogGeneration !== catalog.generation ||
|
||||
binding.catalogDigest !== catalog.catalogDigest ||
|
||||
binding.keyId !== artifact.keyId ||
|
||||
catalog.activeKeyId !== binding.keyId ||
|
||||
requireActiveToolResultKey(catalog).materialProof !==
|
||||
binding.materialProof ||
|
||||
completion.barrierDigest !== requiredText(row, 'joinedBarrierDigest') ||
|
||||
completion.stepRunMutationDigest !==
|
||||
requiredText(row, 'joinedMutationDigest') ||
|
||||
completion.completedStepRunDigest !==
|
||||
requiredText(row, 'joinedCompletedStepRunDigest') ||
|
||||
completion.runEventId !== requiredText(row, 'joinedRunEventId')
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
return Object.freeze({ artifact, binding, completion });
|
||||
}
|
||||
|
||||
async function findRows(
|
||||
queryable: Queryable,
|
||||
where: string,
|
||||
values: readonly unknown[],
|
||||
): Promise<readonly Row[]> {
|
||||
const result = await queryable.query<Row>(
|
||||
`SELECT ${COMPLETION_SELECT}
|
||||
FROM "ql3"."tool_execution_completions" AS completion
|
||||
JOIN "ql3"."tool_execution_start_barriers" AS barrier
|
||||
ON barrier.start_id = completion.start_id
|
||||
JOIN "ql3"."step_run_mutations" AS mutation
|
||||
ON mutation.mutation_id = completion.step_run_mutation_id
|
||||
JOIN "ql3"."run_events" AS event
|
||||
ON event.id = completion.run_event_id
|
||||
LEFT JOIN "ql3"."tool_execution_result_key_bindings" AS binding
|
||||
ON binding.start_id = completion.start_id
|
||||
LEFT JOIN "ql3"."tool_result_key_catalog_generations" AS catalog
|
||||
ON catalog.authority = binding.catalog_authority
|
||||
AND catalog.generation = binding.catalog_generation
|
||||
WHERE ${where}
|
||||
LIMIT 2`,
|
||||
values,
|
||||
);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
async function updateStepRun(
|
||||
client: PostgresClient,
|
||||
mutation: Readonly<StepRunMutation>,
|
||||
): Promise<void> {
|
||||
const step = mutation.stepRun;
|
||||
const result = await client.query(
|
||||
`UPDATE "ql3"."step_runs"
|
||||
SET status = $1, version = $2, attempt_count = $3,
|
||||
output_ref = $4, approval_request_id = $5, ready_at_ms = $6,
|
||||
started_at_ms = $7, finished_at_ms = $8, result_code = $9,
|
||||
error_summary = $10, updated_at_ms = $11,
|
||||
last_mutation_id = $12, step_run_digest = $13,
|
||||
step_run_json = $14::jsonb
|
||||
WHERE id = $15 AND run_id = $16 AND version = $17
|
||||
AND step_run_digest = $18 AND status = $19`,
|
||||
[
|
||||
step.status,
|
||||
step.version,
|
||||
step.attemptCount,
|
||||
step.outputRef,
|
||||
step.approvalRequestId,
|
||||
step.readyAtMs,
|
||||
step.startedAtMs,
|
||||
step.finishedAtMs,
|
||||
step.resultCode,
|
||||
step.errorSummary,
|
||||
step.updatedAtMs,
|
||||
step.lastMutationId,
|
||||
step.stepRunDigest,
|
||||
JSON.stringify(step),
|
||||
step.id,
|
||||
step.runId,
|
||||
mutation.expectedStepRunVersion,
|
||||
mutation.expectedStepRunDigest,
|
||||
mutation.previousStatus,
|
||||
],
|
||||
);
|
||||
if (result.rowCount !== 1) {
|
||||
throw new ToolExecutionCompletionConflictError();
|
||||
}
|
||||
}
|
||||
|
||||
async function updateRun(
|
||||
client: PostgresClient,
|
||||
mutation: Readonly<StepRunMutation>,
|
||||
): Promise<void> {
|
||||
const result = await client.query(
|
||||
`UPDATE "ql3"."runs"
|
||||
SET version = version + 1, event_sequence = event_sequence + 1
|
||||
WHERE id = $1 AND version = $2 AND event_sequence = $3`,
|
||||
[
|
||||
mutation.runId,
|
||||
mutation.expectedRunVersion,
|
||||
mutation.expectedRunEventSequence,
|
||||
],
|
||||
);
|
||||
if (result.rowCount !== 1) {
|
||||
throw new ToolExecutionCompletionConflictError();
|
||||
}
|
||||
}
|
||||
|
||||
async function insertRunEvent(
|
||||
client: PostgresClient,
|
||||
mutation: Readonly<StepRunMutation>,
|
||||
): Promise<void> {
|
||||
const event = mutation.event;
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."run_events" (
|
||||
id, run_id, sequence, type, dedupe_key, actor_type, actor_id,
|
||||
attempt_id, step_run_id, payload, created_at_ms
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, NULL, $8, $9::jsonb, $10
|
||||
)`,
|
||||
[
|
||||
event.id,
|
||||
event.runId,
|
||||
event.sequence,
|
||||
event.type,
|
||||
event.dedupeKey,
|
||||
event.actorType,
|
||||
event.actorId ?? null,
|
||||
mutation.stepRun.id,
|
||||
JSON.stringify(event.payload),
|
||||
event.createdAtMs,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
async function insertMutation(
|
||||
client: PostgresClient,
|
||||
mutation: Readonly<StepRunMutation>,
|
||||
): Promise<void> {
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."step_run_mutations" (
|
||||
mutation_id, mutation_digest, run_id, step_run_id,
|
||||
step_run_digest, event_id, event_sequence, run_version,
|
||||
step_run_json, committed_at_ms
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb,
|
||||
floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
)`,
|
||||
[
|
||||
mutation.mutationId,
|
||||
mutation.mutationDigest,
|
||||
mutation.runId,
|
||||
mutation.stepRun.id,
|
||||
mutation.stepRun.stepRunDigest,
|
||||
mutation.event.id,
|
||||
mutation.event.sequence,
|
||||
mutation.expectedRunVersion + 1,
|
||||
JSON.stringify(mutation.stepRun),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
async function insertCompletion(
|
||||
client: PostgresClient,
|
||||
completion: Readonly<ToolExecutionCompletionRecord>,
|
||||
artifact: Readonly<ToolExecutionResultArtifact>,
|
||||
): Promise<void> {
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."tool_execution_completions" (
|
||||
start_id, artifact_id, project_id, run_id, step_run_id,
|
||||
started_step_run_version, completed_step_run_version,
|
||||
barrier_digest, adapter_digest, output_digest,
|
||||
execution_result_digest, artifact_digest, key_id, algorithm,
|
||||
plaintext_bytes, step_run_mutation_id, step_run_mutation_digest,
|
||||
completed_step_run_digest, run_event_id, completed_at_ms,
|
||||
completion_digest, artifact_json, completion_json
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14,
|
||||
$15, $16, $17, $18, $19, $20, $21, $22::jsonb, $23::jsonb
|
||||
)`,
|
||||
[
|
||||
completion.startId,
|
||||
artifact.artifactId,
|
||||
completion.projectId,
|
||||
completion.runId,
|
||||
completion.stepRunId,
|
||||
completion.startedStepRunVersion,
|
||||
completion.completedStepRunVersion,
|
||||
completion.barrierDigest,
|
||||
completion.adapterDigest,
|
||||
completion.resultArtifact.outputDigest,
|
||||
completion.resultArtifact.executionResultDigest,
|
||||
artifact.artifactDigest,
|
||||
artifact.keyId,
|
||||
artifact.algorithm,
|
||||
artifact.plaintextBytes,
|
||||
completion.stepRunMutationId,
|
||||
completion.stepRunMutationDigest,
|
||||
completion.completedStepRunDigest,
|
||||
completion.runEventId,
|
||||
completion.completedAtMs,
|
||||
completion.completionDigest,
|
||||
JSON.stringify(artifact),
|
||||
JSON.stringify(completion),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
async function resultKeyBindingForCurrentCatalog(
|
||||
client: PostgresClient,
|
||||
command: Readonly<ToolExecutionCompletionCommand>,
|
||||
): Promise<Readonly<ToolExecutionResultKeyBinding>> {
|
||||
const result = await client.query<Row>(
|
||||
`SELECT catalog_json AS "catalogJson"
|
||||
FROM "ql3"."tool_result_key_catalog_generations"
|
||||
WHERE authority = 'trusted-tool-results'
|
||||
ORDER BY generation DESC
|
||||
LIMIT 1`,
|
||||
);
|
||||
if (result.rows.length !== 1) {
|
||||
throw new ToolExecutionCompletionConflictError();
|
||||
}
|
||||
let catalog: Readonly<ToolResultKeyCatalogRecord>;
|
||||
try {
|
||||
catalog = normalizeToolResultKeyCatalogRecord(
|
||||
requiredJson(
|
||||
result.rows[0]!,
|
||||
'catalogJson',
|
||||
) as unknown as ToolResultKeyCatalogRecord,
|
||||
);
|
||||
} catch {
|
||||
throw unavailable();
|
||||
}
|
||||
const active = requireActiveToolResultKey(catalog);
|
||||
if (
|
||||
JSON.stringify(toolResultKeyCatalogFence(catalog, active)) !==
|
||||
JSON.stringify(command.resultKeyCatalogFence)
|
||||
) {
|
||||
throw new ToolExecutionCompletionConflictError();
|
||||
}
|
||||
return toolExecutionResultKeyBinding(command);
|
||||
}
|
||||
|
||||
async function insertResultKeyBinding(
|
||||
client: PostgresClient,
|
||||
binding: Readonly<ToolExecutionResultKeyBinding>,
|
||||
): Promise<void> {
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."tool_execution_result_key_bindings" (
|
||||
start_id, artifact_id, artifact_digest, catalog_authority,
|
||||
catalog_generation, catalog_digest, key_id, material_proof,
|
||||
binding_digest
|
||||
) VALUES (
|
||||
$1, $2, $3, 'trusted-tool-results', $4, $5, $6, $7, $8
|
||||
)`,
|
||||
[
|
||||
binding.startId,
|
||||
binding.artifactId,
|
||||
binding.artifactDigest,
|
||||
binding.catalogGeneration,
|
||||
binding.catalogDigest,
|
||||
binding.keyId,
|
||||
binding.materialProof,
|
||||
binding.bindingDigest,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
export class PostgresToolExecutionCompletionRepository
|
||||
implements ToolExecutionCompletionRepository
|
||||
{
|
||||
constructor(private readonly pool: PostgresPool) {
|
||||
if (
|
||||
!pool ||
|
||||
typeof pool.query !== 'function' ||
|
||||
typeof pool.connect !== 'function'
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
async findByStartId(
|
||||
startIdValue: string,
|
||||
): Promise<Readonly<ToolExecutionCompletionRecord> | null> {
|
||||
const startId = identity(startIdValue, 'start id');
|
||||
try {
|
||||
const rows = await findRows(this.pool, 'completion.start_id = $1', [
|
||||
startId,
|
||||
]);
|
||||
if (rows.length > 1) throw unavailable();
|
||||
return rows[0] ? valuesFromRow(rows[0]).completion : null;
|
||||
} catch (error) {
|
||||
throw mapStorageError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async findResultArtifact(
|
||||
artifactIdValue: string,
|
||||
): Promise<Readonly<ToolExecutionResultArtifact> | null> {
|
||||
const artifactId = identity(artifactIdValue, 'result Artifact id');
|
||||
try {
|
||||
const rows = await findRows(this.pool, 'completion.artifact_id = $1', [
|
||||
artifactId,
|
||||
]);
|
||||
if (rows.length > 1) throw unavailable();
|
||||
return rows[0] ? valuesFromRow(rows[0]).artifact : null;
|
||||
} catch (error) {
|
||||
throw mapStorageError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async commit(
|
||||
commandValue: ToolExecutionCompletionCommand,
|
||||
): Promise<Readonly<CommitToolExecutionCompletionResult>> {
|
||||
const command = normalizeToolExecutionCompletionCommand(commandValue);
|
||||
const completion = toolExecutionCompletionRecord(command);
|
||||
for (
|
||||
let attempt = 0;
|
||||
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
|
||||
attempt += 1
|
||||
) {
|
||||
let client: PostgresClient;
|
||||
try {
|
||||
client = await this.pool.connect();
|
||||
} catch (error) {
|
||||
throw unavailable(error);
|
||||
}
|
||||
let began = false;
|
||||
try {
|
||||
await configurePostgresDefinitionTransaction(client);
|
||||
began = true;
|
||||
await client.query(CATALOG_TRANSACTION_LOCK);
|
||||
const existing = await findRows(
|
||||
client,
|
||||
`completion.start_id = $1
|
||||
OR completion.artifact_id = $2
|
||||
OR completion.step_run_mutation_id = $3
|
||||
OR completion.run_event_id = $4`,
|
||||
[
|
||||
completion.startId,
|
||||
completion.resultArtifact.artifactId,
|
||||
completion.stepRunMutationId,
|
||||
completion.runEventId,
|
||||
],
|
||||
);
|
||||
if (existing.length > 1) {
|
||||
throw new ToolExecutionCompletionConflictError();
|
||||
}
|
||||
if (existing[0]) {
|
||||
const stored = valuesFromRow(existing[0]);
|
||||
if (
|
||||
JSON.stringify(stored.completion) !== JSON.stringify(completion) ||
|
||||
JSON.stringify(stored.artifact) !==
|
||||
JSON.stringify(command.resultArtifact) ||
|
||||
JSON.stringify(stored.binding) !==
|
||||
JSON.stringify(toolExecutionResultKeyBinding(command))
|
||||
) {
|
||||
throw new ToolExecutionCompletionConflictError();
|
||||
}
|
||||
await client.query('COMMIT');
|
||||
began = false;
|
||||
return Object.freeze({
|
||||
status: 'existing',
|
||||
completion: stored.completion,
|
||||
});
|
||||
}
|
||||
|
||||
const failureConflict = await client.query(
|
||||
`SELECT 1
|
||||
FROM "ql3"."tool_execution_failure_completions"
|
||||
WHERE start_id = $1 OR step_run_mutation_id = $2
|
||||
OR run_event_id = $3
|
||||
OR (
|
||||
run_id = $4 AND step_run_id = $5
|
||||
AND completed_step_run_version = $6
|
||||
)
|
||||
LIMIT 1`,
|
||||
[
|
||||
completion.startId,
|
||||
completion.stepRunMutationId,
|
||||
completion.runEventId,
|
||||
completion.runId,
|
||||
completion.stepRunId,
|
||||
completion.completedStepRunVersion,
|
||||
],
|
||||
);
|
||||
if (failureConflict.rows.length > 0) {
|
||||
throw new ToolExecutionCompletionConflictError();
|
||||
}
|
||||
|
||||
const mutation = command.stepRunMutation;
|
||||
const resultKeyBinding = await resultKeyBindingForCurrentCatalog(
|
||||
client,
|
||||
command,
|
||||
);
|
||||
const current = await client.query<Row>(
|
||||
`SELECT
|
||||
barrier.barrier_json AS "barrierJson",
|
||||
start_mutation.run_version AS "startedRunVersion",
|
||||
start_mutation.event_sequence AS "startedEventSequence",
|
||||
step.kind AS "stepKind", step.status AS "stepStatus",
|
||||
step.version AS "stepVersion",
|
||||
step.step_run_digest AS "stepDigest",
|
||||
run.project_id AS "projectId", run.status AS "runStatus",
|
||||
run.version AS "runVersion",
|
||||
run.event_sequence AS "runEventSequence"
|
||||
FROM "ql3"."tool_execution_start_barriers" AS barrier
|
||||
JOIN "ql3"."step_run_mutations" AS start_mutation
|
||||
ON start_mutation.mutation_id = barrier.step_run_mutation_id
|
||||
JOIN "ql3"."step_runs" AS step
|
||||
ON step.id = barrier.step_run_id
|
||||
AND step.run_id = barrier.run_id
|
||||
JOIN "ql3"."runs" AS run ON run.id = barrier.run_id
|
||||
WHERE barrier.start_id = $1
|
||||
LIMIT 2
|
||||
FOR UPDATE OF step, run`,
|
||||
[completion.startId],
|
||||
);
|
||||
const row = current.rows[0];
|
||||
let storedBarrier;
|
||||
try {
|
||||
storedBarrier = row
|
||||
? normalizeToolExecutionStartBarrierRecord(
|
||||
requiredJson(
|
||||
row,
|
||||
'barrierJson',
|
||||
) as unknown as ToolExecutionCompletionCommand['barrier'],
|
||||
)
|
||||
: null;
|
||||
} catch {
|
||||
throw unavailable();
|
||||
}
|
||||
if (
|
||||
current.rows.length !== 1 ||
|
||||
!row ||
|
||||
!storedBarrier ||
|
||||
JSON.stringify(storedBarrier) !== JSON.stringify(command.barrier) ||
|
||||
requiredInteger(row, 'startedRunVersion') !==
|
||||
mutation.expectedRunVersion ||
|
||||
requiredInteger(row, 'startedEventSequence') !==
|
||||
mutation.expectedRunEventSequence ||
|
||||
requiredText(row, 'stepKind') !== 'tool' ||
|
||||
requiredText(row, 'stepStatus') !== 'running' ||
|
||||
requiredInteger(row, 'stepVersion') !==
|
||||
mutation.expectedStepRunVersion ||
|
||||
requiredText(row, 'stepDigest') !== mutation.expectedStepRunDigest ||
|
||||
requiredText(row, 'projectId') !== completion.projectId ||
|
||||
requiredInteger(row, 'runVersion') !== mutation.expectedRunVersion ||
|
||||
requiredInteger(row, 'runEventSequence') !==
|
||||
mutation.expectedRunEventSequence ||
|
||||
TERMINAL_RUN_STATUSES.has(requiredText(row, 'runStatus'))
|
||||
) {
|
||||
throw new ToolExecutionCompletionConflictError();
|
||||
}
|
||||
|
||||
await updateStepRun(client, mutation);
|
||||
await updateRun(client, mutation);
|
||||
await insertRunEvent(client, mutation);
|
||||
await insertMutation(client, mutation);
|
||||
await insertCompletion(client, completion, command.resultArtifact);
|
||||
await insertResultKeyBinding(client, resultKeyBinding);
|
||||
await client.query('COMMIT');
|
||||
began = false;
|
||||
return Object.freeze({ status: 'created', completion });
|
||||
} catch (error) {
|
||||
if (began) await rollbackPostgresDefinitionTransaction(client);
|
||||
const state = postgresSqlState(error);
|
||||
if (
|
||||
state &&
|
||||
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
|
||||
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
throw mapStorageError(error);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
import {
|
||||
InvalidToolExecutionEvidenceError,
|
||||
ToolExecutionEvidenceConflictError,
|
||||
ToolExecutionEvidenceUnavailableError,
|
||||
normalizeListToolExecutionEvidenceQuery,
|
||||
normalizeListToolExecutionEvidenceResult,
|
||||
normalizeToolExecutionEvidenceBundle,
|
||||
type ListToolExecutionEvidenceQuery,
|
||||
type ListToolExecutionEvidenceResult,
|
||||
type PrepareToolExecutionEvidenceResult,
|
||||
type ToolExecutionEvidenceBundle,
|
||||
type ToolExecutionEvidenceRepository,
|
||||
} from '@qinglong/runtime-core/tool-execution-evidence';
|
||||
import type {
|
||||
PostgresClient,
|
||||
PostgresPool,
|
||||
PostgresQueryable,
|
||||
} from '@qinglong/runtime-core';
|
||||
|
||||
import {
|
||||
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
|
||||
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
|
||||
configurePostgresDefinitionTransaction,
|
||||
postgresRequiredInteger,
|
||||
postgresRequiredJsonObject,
|
||||
postgresRequiredString,
|
||||
postgresSqlState,
|
||||
rollbackPostgresDefinitionTransaction,
|
||||
} from '../repository/definitionRepositorySupport';
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
|
||||
const TRACE_ID_PATTERN = /^[0-9a-f]{32}$/;
|
||||
const SPAN_ID_PATTERN = /^[0-9a-f]{16}$/;
|
||||
const AUDIT_EVENT_ID_PATTERN =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
||||
|
||||
const EVIDENCE_SELECT = `
|
||||
trace.trace_json AS "traceJson",
|
||||
receipt.audit_json AS "auditJson",
|
||||
receipt.receipt_json AS "receiptJson",
|
||||
trace.trace_id AS "storedTraceId",
|
||||
trace.span_id AS "storedSpanId",
|
||||
trace.parent_span_id AS "storedParentSpanId",
|
||||
trace.project_id AS "traceProjectId",
|
||||
trace.run_id AS "traceRunId",
|
||||
trace.step_run_id AS "traceStepRunId",
|
||||
trace.invocation_plan_digest AS "traceInvocationPlanDigest",
|
||||
trace.binding_digest AS "traceBindingDigest",
|
||||
trace.adapter_digest AS "storedAdapterDigest",
|
||||
trace.redaction_contract_digest AS "storedRedactionContractDigest",
|
||||
trace.audit_contract_digest AS "storedAuditContractDigest",
|
||||
trace.created_at_ms AS "traceCreatedAtMs",
|
||||
trace.trace_digest AS "storedTraceDigest",
|
||||
receipt.event_id AS "storedEventId",
|
||||
receipt.project_id AS "receiptProjectId",
|
||||
receipt.run_id AS "receiptRunId",
|
||||
receipt.step_run_id AS "receiptStepRunId",
|
||||
receipt.trace_id AS "receiptTraceId",
|
||||
receipt.span_id AS "receiptSpanId",
|
||||
receipt.trace_digest AS "receiptTraceDigest",
|
||||
receipt.invocation_plan_digest AS "receiptInvocationPlanDigest",
|
||||
receipt.binding_digest AS "receiptBindingDigest",
|
||||
receipt.audit_record_digest AS "storedAuditRecordDigest",
|
||||
receipt.created_at_ms AS "receiptCreatedAtMs",
|
||||
receipt.receipt_digest AS "storedReceiptDigest"
|
||||
`;
|
||||
|
||||
function unavailable(
|
||||
cause?: unknown,
|
||||
): ToolExecutionEvidenceUnavailableError {
|
||||
return new ToolExecutionEvidenceUnavailableError({
|
||||
cause: cause instanceof Error ? cause : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function identity(value: unknown, pattern: RegExp, label: string): string {
|
||||
if (typeof value !== 'string' || !pattern.test(value)) {
|
||||
throw new InvalidToolExecutionEvidenceError(`${label} is invalid`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function requiredText(row: Row, key: string): string {
|
||||
return postgresRequiredString(row[key], unavailable);
|
||||
}
|
||||
|
||||
function requiredInteger(row: Row, key: string): number {
|
||||
const value = postgresRequiredInteger(row[key], unavailable);
|
||||
if (value < 0) throw unavailable();
|
||||
return value;
|
||||
}
|
||||
|
||||
function nullableText(row: Row, key: string): string | null {
|
||||
const value = row[key];
|
||||
if (value === null) return null;
|
||||
return postgresRequiredString(value, unavailable);
|
||||
}
|
||||
|
||||
function jsonObject(row: Row, key: string): Readonly<Record<string, unknown>> {
|
||||
return postgresRequiredJsonObject(row[key], unavailable);
|
||||
}
|
||||
|
||||
function bundleFromRow(
|
||||
row: Row,
|
||||
): Readonly<ToolExecutionEvidenceBundle> {
|
||||
let bundle: Readonly<ToolExecutionEvidenceBundle>;
|
||||
try {
|
||||
bundle = normalizeToolExecutionEvidenceBundle({
|
||||
schema: 'qinglong/tool-execution-evidence-bundle@v1',
|
||||
trace: jsonObject(row, 'traceJson'),
|
||||
audit: jsonObject(row, 'auditJson'),
|
||||
receipt: jsonObject(row, 'receiptJson'),
|
||||
} as unknown as ToolExecutionEvidenceBundle);
|
||||
} catch {
|
||||
throw unavailable();
|
||||
}
|
||||
const trace = bundle.trace;
|
||||
const receipt = bundle.receipt;
|
||||
if (
|
||||
requiredText(row, 'storedTraceId') !== trace.traceId ||
|
||||
requiredText(row, 'storedSpanId') !== trace.spanId ||
|
||||
nullableText(row, 'storedParentSpanId') !== trace.parentSpanId ||
|
||||
requiredText(row, 'traceProjectId') !== trace.projectId ||
|
||||
requiredText(row, 'traceRunId') !== trace.runId ||
|
||||
requiredText(row, 'traceStepRunId') !== trace.stepRunId ||
|
||||
requiredText(row, 'traceInvocationPlanDigest') !==
|
||||
trace.invocationPlanDigest ||
|
||||
requiredText(row, 'traceBindingDigest') !== trace.bindingDigest ||
|
||||
requiredText(row, 'storedAdapterDigest') !== trace.adapterDigest ||
|
||||
requiredText(row, 'storedRedactionContractDigest') !==
|
||||
trace.redactionContractDigest ||
|
||||
requiredText(row, 'storedAuditContractDigest') !==
|
||||
trace.auditContractDigest ||
|
||||
requiredInteger(row, 'traceCreatedAtMs') !== trace.createdAtMs ||
|
||||
requiredText(row, 'storedTraceDigest') !== trace.traceDigest ||
|
||||
requiredText(row, 'storedEventId') !== receipt.eventId ||
|
||||
requiredText(row, 'receiptProjectId') !== receipt.projectId ||
|
||||
requiredText(row, 'receiptRunId') !== receipt.runId ||
|
||||
requiredText(row, 'receiptStepRunId') !== receipt.stepRunId ||
|
||||
requiredText(row, 'receiptTraceId') !== receipt.traceId ||
|
||||
requiredText(row, 'receiptSpanId') !== receipt.spanId ||
|
||||
requiredText(row, 'receiptTraceDigest') !== receipt.traceDigest ||
|
||||
requiredText(row, 'receiptInvocationPlanDigest') !==
|
||||
receipt.invocationPlanDigest ||
|
||||
requiredText(row, 'receiptBindingDigest') !== receipt.bindingDigest ||
|
||||
requiredText(row, 'storedAuditRecordDigest') !==
|
||||
receipt.auditRecordDigest ||
|
||||
requiredInteger(row, 'receiptCreatedAtMs') !== receipt.createdAtMs ||
|
||||
requiredText(row, 'storedReceiptDigest') !== receipt.receiptDigest
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
return bundle;
|
||||
}
|
||||
|
||||
function constraintError(error: unknown): boolean {
|
||||
const state = postgresSqlState(error);
|
||||
return state === '23503' || state === '23505' || state === '23514';
|
||||
}
|
||||
|
||||
function mapStorageError(error: unknown): Error {
|
||||
if (
|
||||
error instanceof InvalidToolExecutionEvidenceError ||
|
||||
error instanceof ToolExecutionEvidenceConflictError ||
|
||||
error instanceof ToolExecutionEvidenceUnavailableError
|
||||
) {
|
||||
return error;
|
||||
}
|
||||
return constraintError(error)
|
||||
? new ToolExecutionEvidenceConflictError()
|
||||
: unavailable(error);
|
||||
}
|
||||
|
||||
async function findRows(
|
||||
queryable: PostgresQueryable,
|
||||
where: string,
|
||||
values: readonly unknown[],
|
||||
limit = 2,
|
||||
): Promise<readonly Row[]> {
|
||||
const result = await queryable.query<Row>(
|
||||
`SELECT ${EVIDENCE_SELECT}
|
||||
FROM "ql3"."tool_execution_trace_anchors" AS trace
|
||||
JOIN "ql3"."tool_execution_audit_receipts" AS receipt
|
||||
ON receipt.trace_id = trace.trace_id
|
||||
AND receipt.span_id = trace.span_id
|
||||
WHERE ${where}
|
||||
LIMIT ${limit}`,
|
||||
values,
|
||||
);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
async function insertAudit(
|
||||
client: PostgresClient,
|
||||
bundle: Readonly<ToolExecutionEvidenceBundle>,
|
||||
): Promise<void> {
|
||||
const audit = bundle.audit;
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."security_audit_events" (
|
||||
event_id, request_id, operation_id, project_id, subject_type,
|
||||
subject_id, authentication_id, outcome, reasons, project_version,
|
||||
binding_version, occurred_at_ms
|
||||
) VALUES (
|
||||
$1::uuid, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11, $12
|
||||
)`,
|
||||
[
|
||||
audit.eventId,
|
||||
audit.requestId,
|
||||
audit.operationId,
|
||||
audit.projectId,
|
||||
audit.subject?.type ?? null,
|
||||
audit.subject?.id ?? null,
|
||||
audit.authenticationId,
|
||||
audit.outcome,
|
||||
JSON.stringify(audit.reasons),
|
||||
audit.fence?.projectVersion ?? null,
|
||||
audit.fence?.bindingVersion ?? null,
|
||||
audit.occurredAtMs,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
async function insertTraceAndReceipt(
|
||||
client: PostgresClient,
|
||||
bundle: Readonly<ToolExecutionEvidenceBundle>,
|
||||
): Promise<void> {
|
||||
const trace = bundle.trace;
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."tool_execution_trace_anchors" (
|
||||
trace_id, span_id, parent_span_id, project_id, run_id, step_run_id,
|
||||
invocation_plan_digest, binding_digest, adapter_digest,
|
||||
redaction_contract_digest, audit_contract_digest, created_at_ms,
|
||||
trace_digest, trace_json
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
|
||||
$14::jsonb
|
||||
)`,
|
||||
[
|
||||
trace.traceId,
|
||||
trace.spanId,
|
||||
trace.parentSpanId,
|
||||
trace.projectId,
|
||||
trace.runId,
|
||||
trace.stepRunId,
|
||||
trace.invocationPlanDigest,
|
||||
trace.bindingDigest,
|
||||
trace.adapterDigest,
|
||||
trace.redactionContractDigest,
|
||||
trace.auditContractDigest,
|
||||
trace.createdAtMs,
|
||||
trace.traceDigest,
|
||||
JSON.stringify(trace),
|
||||
],
|
||||
);
|
||||
const receipt = bundle.receipt;
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."tool_execution_audit_receipts" (
|
||||
event_id, project_id, run_id, step_run_id, trace_id, span_id,
|
||||
trace_digest, invocation_plan_digest, binding_digest,
|
||||
audit_record_digest, created_at_ms, receipt_digest, audit_json,
|
||||
receipt_json
|
||||
) VALUES (
|
||||
$1::uuid, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12,
|
||||
$13::jsonb, $14::jsonb
|
||||
)`,
|
||||
[
|
||||
receipt.eventId,
|
||||
receipt.projectId,
|
||||
receipt.runId,
|
||||
receipt.stepRunId,
|
||||
receipt.traceId,
|
||||
receipt.spanId,
|
||||
receipt.traceDigest,
|
||||
receipt.invocationPlanDigest,
|
||||
receipt.bindingDigest,
|
||||
receipt.auditRecordDigest,
|
||||
receipt.createdAtMs,
|
||||
receipt.receiptDigest,
|
||||
JSON.stringify(bundle.audit),
|
||||
JSON.stringify(receipt),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
export class PostgresToolExecutionEvidenceRepository
|
||||
implements ToolExecutionEvidenceRepository
|
||||
{
|
||||
constructor(private readonly pool: PostgresPool) {
|
||||
if (
|
||||
!pool ||
|
||||
typeof pool.query !== 'function' ||
|
||||
typeof pool.connect !== 'function'
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
async findByTrace(
|
||||
traceIdValue: string,
|
||||
spanIdValue: string,
|
||||
): Promise<Readonly<ToolExecutionEvidenceBundle> | null> {
|
||||
const traceId = identity(traceIdValue, TRACE_ID_PATTERN, 'traceId');
|
||||
const spanId = identity(spanIdValue, SPAN_ID_PATTERN, 'spanId');
|
||||
try {
|
||||
const rows = await findRows(
|
||||
this.pool,
|
||||
'trace.trace_id = $1 AND trace.span_id = $2',
|
||||
[traceId, spanId],
|
||||
);
|
||||
if (rows.length > 1) throw unavailable();
|
||||
return rows[0] ? bundleFromRow(rows[0]) : null;
|
||||
} catch (error) {
|
||||
throw mapStorageError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async findByAuditEventId(
|
||||
eventIdValue: string,
|
||||
): Promise<Readonly<ToolExecutionEvidenceBundle> | null> {
|
||||
const eventId = identity(
|
||||
eventIdValue,
|
||||
AUDIT_EVENT_ID_PATTERN,
|
||||
'audit eventId',
|
||||
);
|
||||
try {
|
||||
const rows = await findRows(
|
||||
this.pool,
|
||||
'receipt.event_id = $1::uuid',
|
||||
[eventId],
|
||||
);
|
||||
if (rows.length > 1) throw unavailable();
|
||||
return rows[0] ? bundleFromRow(rows[0]) : null;
|
||||
} catch (error) {
|
||||
throw mapStorageError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async listByRun(
|
||||
queryValue: ListToolExecutionEvidenceQuery,
|
||||
): Promise<ListToolExecutionEvidenceResult> {
|
||||
const query = normalizeListToolExecutionEvidenceQuery(queryValue);
|
||||
try {
|
||||
const result = await this.pool.query<Row>(
|
||||
`SELECT ${EVIDENCE_SELECT}
|
||||
FROM "ql3"."tool_execution_trace_anchors" AS trace
|
||||
JOIN "ql3"."tool_execution_audit_receipts" AS receipt
|
||||
ON receipt.trace_id = trace.trace_id
|
||||
AND receipt.span_id = trace.span_id
|
||||
WHERE trace.run_id = $1 AND (
|
||||
$2::char(32) IS NULL OR trace.created_at_ms > $3 OR
|
||||
(trace.created_at_ms = $3 AND trace.trace_id > $2) OR
|
||||
(trace.created_at_ms = $3 AND trace.trace_id = $2
|
||||
AND trace.span_id > $4)
|
||||
)
|
||||
ORDER BY trace.created_at_ms, trace.trace_id, trace.span_id
|
||||
LIMIT $5`,
|
||||
[
|
||||
query.runId,
|
||||
query.after?.traceId ?? null,
|
||||
query.after?.createdAtMs ?? 0,
|
||||
query.after?.spanId ?? '',
|
||||
query.limit + 1,
|
||||
],
|
||||
);
|
||||
const truncated = result.rows.length > query.limit;
|
||||
const bundles = result.rows.slice(0, query.limit).map(bundleFromRow);
|
||||
const last = bundles.at(-1);
|
||||
return normalizeListToolExecutionEvidenceResult(
|
||||
{
|
||||
bundles,
|
||||
truncated,
|
||||
...(truncated && last
|
||||
? {
|
||||
next: {
|
||||
createdAtMs: last.trace.createdAtMs,
|
||||
traceId: last.trace.traceId,
|
||||
spanId: last.trace.spanId,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
query,
|
||||
);
|
||||
} catch (error) {
|
||||
throw mapStorageError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async prepare(
|
||||
bundleValue: ToolExecutionEvidenceBundle,
|
||||
): Promise<Readonly<PrepareToolExecutionEvidenceResult>> {
|
||||
const bundle = normalizeToolExecutionEvidenceBundle(bundleValue);
|
||||
for (
|
||||
let attempt = 0;
|
||||
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
|
||||
attempt += 1
|
||||
) {
|
||||
let client: PostgresClient;
|
||||
try {
|
||||
client = await this.pool.connect();
|
||||
} catch (error) {
|
||||
throw unavailable(error);
|
||||
}
|
||||
let began = false;
|
||||
try {
|
||||
await configurePostgresDefinitionTransaction(client);
|
||||
began = true;
|
||||
const existing = await findRows(
|
||||
client,
|
||||
'receipt.event_id = $1::uuid OR (trace.trace_id = $2 AND trace.span_id = $3)',
|
||||
[
|
||||
bundle.receipt.eventId,
|
||||
bundle.trace.traceId,
|
||||
bundle.trace.spanId,
|
||||
],
|
||||
3,
|
||||
);
|
||||
if (existing.length > 1) {
|
||||
throw new ToolExecutionEvidenceConflictError();
|
||||
}
|
||||
if (existing[0]) {
|
||||
const stored = bundleFromRow(existing[0]);
|
||||
if (JSON.stringify(stored) !== JSON.stringify(bundle)) {
|
||||
throw new ToolExecutionEvidenceConflictError();
|
||||
}
|
||||
await client.query('COMMIT');
|
||||
began = false;
|
||||
return Object.freeze({ status: 'existing', bundle: stored });
|
||||
}
|
||||
|
||||
const step = await client.query<Row>(
|
||||
`SELECT step.kind, step.status, run.project_id AS "projectId"
|
||||
FROM "ql3"."step_runs" AS step
|
||||
JOIN "ql3"."runs" AS run ON run.id = step.run_id
|
||||
WHERE step.id = $1 AND step.run_id = $2
|
||||
LIMIT 2
|
||||
FOR SHARE OF step, run`,
|
||||
[bundle.trace.stepRunId, bundle.trace.runId],
|
||||
);
|
||||
if (
|
||||
step.rows.length !== 1 ||
|
||||
requiredText(step.rows[0]!, 'kind') !== 'tool' ||
|
||||
!['ready', 'waiting_approval'].includes(
|
||||
requiredText(step.rows[0]!, 'status'),
|
||||
) ||
|
||||
requiredText(step.rows[0]!, 'projectId') !==
|
||||
bundle.trace.projectId
|
||||
) {
|
||||
throw new ToolExecutionEvidenceConflictError();
|
||||
}
|
||||
|
||||
await insertAudit(client, bundle);
|
||||
await insertTraceAndReceipt(client, bundle);
|
||||
await client.query('COMMIT');
|
||||
began = false;
|
||||
return Object.freeze({ status: 'created', bundle });
|
||||
} catch (error) {
|
||||
if (began) await rollbackPostgresDefinitionTransaction(client);
|
||||
const state = postgresSqlState(error);
|
||||
if (
|
||||
((state &&
|
||||
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state)) ||
|
||||
state === '23505') &&
|
||||
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
throw mapStorageError(error);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
+533
@@ -0,0 +1,533 @@
|
||||
import type {
|
||||
PostgresClient,
|
||||
PostgresPool,
|
||||
PostgresQueryable,
|
||||
} from '@qinglong/runtime-core';
|
||||
import {
|
||||
InvalidToolExecutionFailureCompletionError,
|
||||
MAX_TOOL_EXECUTION_FAILURE_COMPLETION_JSON_BYTES,
|
||||
ToolExecutionFailureCompletionConflictError,
|
||||
ToolExecutionFailureCompletionUnavailableError,
|
||||
normalizeToolExecutionFailureCompletionCommand,
|
||||
normalizeToolExecutionFailureCompletionRecord,
|
||||
toolExecutionFailureCompletionRecord,
|
||||
type CommitToolExecutionFailureCompletionResult,
|
||||
type ToolExecutionFailureCompletionCommand,
|
||||
type ToolExecutionFailureCompletionRecord,
|
||||
type ToolExecutionFailureCompletionRepository,
|
||||
} from '@qinglong/runtime-core/tool-execution-failure-completion';
|
||||
import type { StepRunMutation } from '@qinglong/runtime-core/step-run';
|
||||
import { normalizeToolExecutionStartBarrierRecord } from '@qinglong/runtime-core/tool-execution-start-barrier';
|
||||
|
||||
import {
|
||||
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
|
||||
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
|
||||
configurePostgresDefinitionTransaction,
|
||||
postgresRequiredInteger,
|
||||
postgresRequiredJsonObject,
|
||||
postgresRequiredString,
|
||||
postgresSqlState,
|
||||
rollbackPostgresDefinitionTransaction,
|
||||
} from '../repository/definitionRepositorySupport';
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
type Queryable = Pick<PostgresQueryable, 'query'>;
|
||||
|
||||
const IDENTITY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
const TERMINAL_RUN_STATUSES = new Set([
|
||||
'succeeded',
|
||||
'failed',
|
||||
'cancelled',
|
||||
'timed_out',
|
||||
]);
|
||||
const COMPLETION_SELECT = `
|
||||
completion.completion_json AS "completionJson",
|
||||
completion.start_id AS "storedStartId",
|
||||
completion.project_id AS "storedProjectId",
|
||||
completion.run_id AS "storedRunId",
|
||||
completion.step_run_id AS "storedStepRunId",
|
||||
completion.started_step_run_version AS "storedStartedStepRunVersion",
|
||||
completion.completed_step_run_version AS "storedCompletedStepRunVersion",
|
||||
completion.barrier_digest AS "storedBarrierDigest",
|
||||
completion.adapter_digest AS "storedAdapterDigest",
|
||||
completion.outcome AS "storedOutcome",
|
||||
completion.result_code AS "storedResultCode",
|
||||
completion.error_summary AS "storedErrorSummary",
|
||||
completion.step_run_mutation_id AS "storedMutationId",
|
||||
completion.step_run_mutation_digest AS "storedMutationDigest",
|
||||
completion.completed_step_run_digest AS "storedCompletedStepRunDigest",
|
||||
completion.run_event_id AS "storedRunEventId",
|
||||
completion.completed_at_ms AS "storedCompletedAtMs",
|
||||
completion.completion_digest AS "storedCompletionDigest",
|
||||
barrier.barrier_digest AS "joinedBarrierDigest",
|
||||
mutation.mutation_digest AS "joinedMutationDigest",
|
||||
mutation.step_run_digest AS "joinedCompletedStepRunDigest",
|
||||
event.id AS "joinedRunEventId"
|
||||
`;
|
||||
|
||||
function unavailable(
|
||||
cause?: unknown,
|
||||
): ToolExecutionFailureCompletionUnavailableError {
|
||||
return new ToolExecutionFailureCompletionUnavailableError({
|
||||
cause: cause instanceof Error ? cause : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function requiredText(row: Row, key: string): string {
|
||||
return postgresRequiredString(row[key], unavailable);
|
||||
}
|
||||
|
||||
function requiredInteger(row: Row, key: string): number {
|
||||
return postgresRequiredInteger(row[key], unavailable);
|
||||
}
|
||||
|
||||
function requiredJson(row: Row, key: string): Record<string, unknown> {
|
||||
return postgresRequiredJsonObject(row[key], unavailable);
|
||||
}
|
||||
|
||||
function identity(value: unknown, label: string): string {
|
||||
if (typeof value !== 'string' || !IDENTITY_PATTERN.test(value)) {
|
||||
throw new InvalidToolExecutionFailureCompletionError(`${label} is invalid`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function constraintError(error: unknown): boolean {
|
||||
const state = postgresSqlState(error);
|
||||
return state === '23503' || state === '23505' || state === '23514';
|
||||
}
|
||||
|
||||
function mapStorageError(error: unknown): Error {
|
||||
if (
|
||||
error instanceof InvalidToolExecutionFailureCompletionError ||
|
||||
error instanceof ToolExecutionFailureCompletionConflictError ||
|
||||
error instanceof ToolExecutionFailureCompletionUnavailableError
|
||||
) {
|
||||
return error;
|
||||
}
|
||||
return constraintError(error)
|
||||
? new ToolExecutionFailureCompletionConflictError()
|
||||
: unavailable(error);
|
||||
}
|
||||
|
||||
function valuesFromRow(
|
||||
row: Row,
|
||||
): Readonly<ToolExecutionFailureCompletionRecord> {
|
||||
let completion: Readonly<ToolExecutionFailureCompletionRecord>;
|
||||
try {
|
||||
completion = normalizeToolExecutionFailureCompletionRecord(
|
||||
requiredJson(
|
||||
row,
|
||||
'completionJson',
|
||||
) as unknown as ToolExecutionFailureCompletionRecord,
|
||||
);
|
||||
} catch {
|
||||
throw unavailable();
|
||||
}
|
||||
if (
|
||||
Buffer.byteLength(JSON.stringify(completion), 'utf8') >
|
||||
MAX_TOOL_EXECUTION_FAILURE_COMPLETION_JSON_BYTES ||
|
||||
completion.startId !== requiredText(row, 'storedStartId') ||
|
||||
completion.projectId !== requiredText(row, 'storedProjectId') ||
|
||||
completion.runId !== requiredText(row, 'storedRunId') ||
|
||||
completion.stepRunId !== requiredText(row, 'storedStepRunId') ||
|
||||
completion.startedStepRunVersion !==
|
||||
requiredInteger(row, 'storedStartedStepRunVersion') ||
|
||||
completion.completedStepRunVersion !==
|
||||
requiredInteger(row, 'storedCompletedStepRunVersion') ||
|
||||
completion.barrierDigest !== requiredText(row, 'storedBarrierDigest') ||
|
||||
completion.adapterDigest !== requiredText(row, 'storedAdapterDigest') ||
|
||||
completion.outcome !== requiredText(row, 'storedOutcome') ||
|
||||
completion.resultCode !== requiredText(row, 'storedResultCode') ||
|
||||
completion.errorSummary !== requiredText(row, 'storedErrorSummary') ||
|
||||
completion.stepRunMutationId !== requiredText(row, 'storedMutationId') ||
|
||||
completion.stepRunMutationDigest !==
|
||||
requiredText(row, 'storedMutationDigest') ||
|
||||
completion.completedStepRunDigest !==
|
||||
requiredText(row, 'storedCompletedStepRunDigest') ||
|
||||
completion.runEventId !== requiredText(row, 'storedRunEventId') ||
|
||||
completion.completedAtMs !== requiredInteger(row, 'storedCompletedAtMs') ||
|
||||
completion.completionDigest !==
|
||||
requiredText(row, 'storedCompletionDigest') ||
|
||||
completion.barrierDigest !== requiredText(row, 'joinedBarrierDigest') ||
|
||||
completion.stepRunMutationDigest !==
|
||||
requiredText(row, 'joinedMutationDigest') ||
|
||||
completion.completedStepRunDigest !==
|
||||
requiredText(row, 'joinedCompletedStepRunDigest') ||
|
||||
completion.runEventId !== requiredText(row, 'joinedRunEventId')
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
return completion;
|
||||
}
|
||||
|
||||
async function findRows(
|
||||
queryable: Queryable,
|
||||
where: string,
|
||||
values: readonly unknown[],
|
||||
): Promise<readonly Row[]> {
|
||||
const result = await queryable.query<Row>(
|
||||
`SELECT ${COMPLETION_SELECT}
|
||||
FROM "ql3"."tool_execution_failure_completions" AS completion
|
||||
JOIN "ql3"."tool_execution_start_barriers" AS barrier
|
||||
ON barrier.start_id = completion.start_id
|
||||
JOIN "ql3"."step_run_mutations" AS mutation
|
||||
ON mutation.mutation_id = completion.step_run_mutation_id
|
||||
JOIN "ql3"."run_events" AS event
|
||||
ON event.id = completion.run_event_id
|
||||
WHERE ${where}
|
||||
LIMIT 2`,
|
||||
values,
|
||||
);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
async function updateStepRun(
|
||||
client: PostgresClient,
|
||||
mutation: Readonly<StepRunMutation>,
|
||||
): Promise<void> {
|
||||
const step = mutation.stepRun;
|
||||
const result = await client.query(
|
||||
`UPDATE "ql3"."step_runs"
|
||||
SET status = $1, version = $2, attempt_count = $3,
|
||||
output_ref = $4, approval_request_id = $5, ready_at_ms = $6,
|
||||
started_at_ms = $7, finished_at_ms = $8, result_code = $9,
|
||||
error_summary = $10, updated_at_ms = $11,
|
||||
last_mutation_id = $12, step_run_digest = $13,
|
||||
step_run_json = $14::jsonb
|
||||
WHERE id = $15 AND run_id = $16 AND version = $17
|
||||
AND step_run_digest = $18 AND status = $19`,
|
||||
[
|
||||
step.status,
|
||||
step.version,
|
||||
step.attemptCount,
|
||||
step.outputRef,
|
||||
step.approvalRequestId,
|
||||
step.readyAtMs,
|
||||
step.startedAtMs,
|
||||
step.finishedAtMs,
|
||||
step.resultCode,
|
||||
step.errorSummary,
|
||||
step.updatedAtMs,
|
||||
step.lastMutationId,
|
||||
step.stepRunDigest,
|
||||
JSON.stringify(step),
|
||||
step.id,
|
||||
step.runId,
|
||||
mutation.expectedStepRunVersion,
|
||||
mutation.expectedStepRunDigest,
|
||||
mutation.previousStatus,
|
||||
],
|
||||
);
|
||||
if (result.rowCount !== 1) {
|
||||
throw new ToolExecutionFailureCompletionConflictError();
|
||||
}
|
||||
}
|
||||
|
||||
async function updateRun(
|
||||
client: PostgresClient,
|
||||
mutation: Readonly<StepRunMutation>,
|
||||
): Promise<void> {
|
||||
const result = await client.query(
|
||||
`UPDATE "ql3"."runs"
|
||||
SET version = version + 1, event_sequence = event_sequence + 1
|
||||
WHERE id = $1 AND version = $2 AND event_sequence = $3`,
|
||||
[
|
||||
mutation.runId,
|
||||
mutation.expectedRunVersion,
|
||||
mutation.expectedRunEventSequence,
|
||||
],
|
||||
);
|
||||
if (result.rowCount !== 1) {
|
||||
throw new ToolExecutionFailureCompletionConflictError();
|
||||
}
|
||||
}
|
||||
|
||||
async function insertRunEvent(
|
||||
client: PostgresClient,
|
||||
mutation: Readonly<StepRunMutation>,
|
||||
): Promise<void> {
|
||||
const event = mutation.event;
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."run_events" (
|
||||
id, run_id, sequence, type, dedupe_key, actor_type, actor_id,
|
||||
attempt_id, step_run_id, payload, created_at_ms
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, NULL, $8, $9::jsonb, $10
|
||||
)`,
|
||||
[
|
||||
event.id,
|
||||
event.runId,
|
||||
event.sequence,
|
||||
event.type,
|
||||
event.dedupeKey,
|
||||
event.actorType,
|
||||
event.actorId ?? null,
|
||||
mutation.stepRun.id,
|
||||
JSON.stringify(event.payload),
|
||||
event.createdAtMs,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
async function insertMutation(
|
||||
client: PostgresClient,
|
||||
mutation: Readonly<StepRunMutation>,
|
||||
): Promise<void> {
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."step_run_mutations" (
|
||||
mutation_id, mutation_digest, run_id, step_run_id,
|
||||
step_run_digest, event_id, event_sequence, run_version,
|
||||
step_run_json, committed_at_ms
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb,
|
||||
floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
)`,
|
||||
[
|
||||
mutation.mutationId,
|
||||
mutation.mutationDigest,
|
||||
mutation.runId,
|
||||
mutation.stepRun.id,
|
||||
mutation.stepRun.stepRunDigest,
|
||||
mutation.event.id,
|
||||
mutation.event.sequence,
|
||||
mutation.expectedRunVersion + 1,
|
||||
JSON.stringify(mutation.stepRun),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
async function insertCompletion(
|
||||
client: PostgresClient,
|
||||
completion: Readonly<ToolExecutionFailureCompletionRecord>,
|
||||
): Promise<void> {
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."tool_execution_failure_completions" (
|
||||
start_id, project_id, run_id, step_run_id,
|
||||
started_step_run_version, completed_step_run_version,
|
||||
barrier_digest, adapter_digest, outcome, result_code,
|
||||
error_summary, step_run_mutation_id, step_run_mutation_digest,
|
||||
completed_step_run_digest, run_event_id, completed_at_ms,
|
||||
completion_digest, completion_json
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14,
|
||||
$15, $16, $17, $18::jsonb
|
||||
)`,
|
||||
[
|
||||
completion.startId,
|
||||
completion.projectId,
|
||||
completion.runId,
|
||||
completion.stepRunId,
|
||||
completion.startedStepRunVersion,
|
||||
completion.completedStepRunVersion,
|
||||
completion.barrierDigest,
|
||||
completion.adapterDigest,
|
||||
completion.outcome,
|
||||
completion.resultCode,
|
||||
completion.errorSummary,
|
||||
completion.stepRunMutationId,
|
||||
completion.stepRunMutationDigest,
|
||||
completion.completedStepRunDigest,
|
||||
completion.runEventId,
|
||||
completion.completedAtMs,
|
||||
completion.completionDigest,
|
||||
JSON.stringify(completion),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
export class PostgresToolExecutionFailureCompletionRepository
|
||||
implements ToolExecutionFailureCompletionRepository
|
||||
{
|
||||
constructor(private readonly pool: PostgresPool) {
|
||||
if (
|
||||
!pool ||
|
||||
typeof pool.query !== 'function' ||
|
||||
typeof pool.connect !== 'function'
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
async findByStartId(
|
||||
startIdValue: string,
|
||||
): Promise<Readonly<ToolExecutionFailureCompletionRecord> | null> {
|
||||
const startId = identity(startIdValue, 'start id');
|
||||
try {
|
||||
const rows = await findRows(this.pool, 'completion.start_id = $1', [
|
||||
startId,
|
||||
]);
|
||||
if (rows.length > 1) throw unavailable();
|
||||
return rows[0] ? valuesFromRow(rows[0]) : null;
|
||||
} catch (error) {
|
||||
throw mapStorageError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async commit(
|
||||
commandValue: ToolExecutionFailureCompletionCommand,
|
||||
): Promise<Readonly<CommitToolExecutionFailureCompletionResult>> {
|
||||
const command =
|
||||
normalizeToolExecutionFailureCompletionCommand(commandValue);
|
||||
const completion = toolExecutionFailureCompletionRecord(command);
|
||||
for (
|
||||
let attempt = 0;
|
||||
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
|
||||
attempt += 1
|
||||
) {
|
||||
let client: PostgresClient;
|
||||
try {
|
||||
client = await this.pool.connect();
|
||||
} catch (error) {
|
||||
throw unavailable(error);
|
||||
}
|
||||
let began = false;
|
||||
try {
|
||||
await configurePostgresDefinitionTransaction(client);
|
||||
began = true;
|
||||
const existing = await findRows(
|
||||
client,
|
||||
`completion.start_id = $1
|
||||
OR completion.step_run_mutation_id = $2
|
||||
OR completion.run_event_id = $3
|
||||
OR (
|
||||
completion.run_id = $4 AND completion.step_run_id = $5
|
||||
AND completion.completed_step_run_version = $6
|
||||
)`,
|
||||
[
|
||||
completion.startId,
|
||||
completion.stepRunMutationId,
|
||||
completion.runEventId,
|
||||
completion.runId,
|
||||
completion.stepRunId,
|
||||
completion.completedStepRunVersion,
|
||||
],
|
||||
);
|
||||
if (existing.length > 1) {
|
||||
throw new ToolExecutionFailureCompletionConflictError();
|
||||
}
|
||||
if (existing[0]) {
|
||||
const stored = valuesFromRow(existing[0]);
|
||||
if (JSON.stringify(stored) !== JSON.stringify(completion)) {
|
||||
throw new ToolExecutionFailureCompletionConflictError();
|
||||
}
|
||||
await client.query('COMMIT');
|
||||
began = false;
|
||||
return Object.freeze({
|
||||
status: 'existing',
|
||||
completion: stored,
|
||||
});
|
||||
}
|
||||
|
||||
const successConflict = await client.query(
|
||||
`SELECT 1
|
||||
FROM "ql3"."tool_execution_completions"
|
||||
WHERE start_id = $1 OR step_run_mutation_id = $2
|
||||
OR run_event_id = $3
|
||||
OR (
|
||||
run_id = $4 AND step_run_id = $5
|
||||
AND completed_step_run_version = $6
|
||||
)
|
||||
LIMIT 1`,
|
||||
[
|
||||
completion.startId,
|
||||
completion.stepRunMutationId,
|
||||
completion.runEventId,
|
||||
completion.runId,
|
||||
completion.stepRunId,
|
||||
completion.completedStepRunVersion,
|
||||
],
|
||||
);
|
||||
if (successConflict.rows.length > 0) {
|
||||
throw new ToolExecutionFailureCompletionConflictError();
|
||||
}
|
||||
|
||||
const mutation = command.stepRunMutation;
|
||||
const current = await client.query<Row>(
|
||||
`SELECT
|
||||
barrier.barrier_json AS "barrierJson",
|
||||
start_mutation.run_version AS "startedRunVersion",
|
||||
start_mutation.event_sequence AS "startedEventSequence",
|
||||
step.kind AS "stepKind", step.status AS "stepStatus",
|
||||
step.version AS "stepVersion",
|
||||
step.step_run_digest AS "stepDigest",
|
||||
run.project_id AS "projectId", run.status AS "runStatus",
|
||||
run.version AS "runVersion",
|
||||
run.event_sequence AS "runEventSequence"
|
||||
FROM "ql3"."tool_execution_start_barriers" AS barrier
|
||||
JOIN "ql3"."step_run_mutations" AS start_mutation
|
||||
ON start_mutation.mutation_id = barrier.step_run_mutation_id
|
||||
JOIN "ql3"."step_runs" AS step
|
||||
ON step.id = barrier.step_run_id
|
||||
AND step.run_id = barrier.run_id
|
||||
JOIN "ql3"."runs" AS run ON run.id = barrier.run_id
|
||||
WHERE barrier.start_id = $1
|
||||
LIMIT 2
|
||||
FOR UPDATE OF step, run`,
|
||||
[completion.startId],
|
||||
);
|
||||
const row = current.rows[0];
|
||||
let storedBarrier;
|
||||
try {
|
||||
storedBarrier = row
|
||||
? normalizeToolExecutionStartBarrierRecord(
|
||||
requiredJson(
|
||||
row,
|
||||
'barrierJson',
|
||||
) as unknown as ToolExecutionFailureCompletionCommand['barrier'],
|
||||
)
|
||||
: null;
|
||||
} catch {
|
||||
throw unavailable();
|
||||
}
|
||||
if (
|
||||
current.rows.length !== 1 ||
|
||||
!row ||
|
||||
!storedBarrier ||
|
||||
JSON.stringify(storedBarrier) !== JSON.stringify(command.barrier) ||
|
||||
requiredInteger(row, 'startedRunVersion') !==
|
||||
mutation.expectedRunVersion ||
|
||||
requiredInteger(row, 'startedEventSequence') !==
|
||||
mutation.expectedRunEventSequence ||
|
||||
requiredText(row, 'stepKind') !== 'tool' ||
|
||||
requiredText(row, 'stepStatus') !== 'running' ||
|
||||
requiredInteger(row, 'stepVersion') !==
|
||||
mutation.expectedStepRunVersion ||
|
||||
requiredText(row, 'stepDigest') !== mutation.expectedStepRunDigest ||
|
||||
requiredText(row, 'projectId') !== completion.projectId ||
|
||||
requiredInteger(row, 'runVersion') !== mutation.expectedRunVersion ||
|
||||
requiredInteger(row, 'runEventSequence') !==
|
||||
mutation.expectedRunEventSequence ||
|
||||
TERMINAL_RUN_STATUSES.has(requiredText(row, 'runStatus'))
|
||||
) {
|
||||
throw new ToolExecutionFailureCompletionConflictError();
|
||||
}
|
||||
|
||||
await updateStepRun(client, mutation);
|
||||
await updateRun(client, mutation);
|
||||
await insertRunEvent(client, mutation);
|
||||
await insertMutation(client, mutation);
|
||||
await insertCompletion(client, completion);
|
||||
await client.query('COMMIT');
|
||||
began = false;
|
||||
return Object.freeze({ status: 'created', completion });
|
||||
} catch (error) {
|
||||
if (began) await rollbackPostgresDefinitionTransaction(client);
|
||||
const state = postgresSqlState(error);
|
||||
if (
|
||||
state &&
|
||||
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
|
||||
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
throw mapStorageError(error);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
+630
@@ -0,0 +1,630 @@
|
||||
import {
|
||||
InvalidToolExecutionStartBarrierError,
|
||||
ToolExecutionStartBarrierConflictError,
|
||||
ToolExecutionStartBarrierUnavailableError,
|
||||
normalizeToolExecutionStartBarrierRecord,
|
||||
normalizeToolExecutionStartCommand,
|
||||
toolExecutionStartBarrierRecord,
|
||||
type PrepareToolExecutionStartResult,
|
||||
type ToolExecutionStartBarrierRecord,
|
||||
type ToolExecutionStartBarrierRepository,
|
||||
type ToolExecutionStartCommand,
|
||||
} from '@qinglong/runtime-core/tool-execution-start-barrier';
|
||||
import type {
|
||||
PostgresClient,
|
||||
PostgresPool,
|
||||
PostgresQueryable,
|
||||
} from '@qinglong/runtime-core';
|
||||
|
||||
import {
|
||||
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
|
||||
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
|
||||
configurePostgresDefinitionTransaction,
|
||||
postgresRequiredInteger,
|
||||
postgresRequiredJsonObject,
|
||||
postgresRequiredString,
|
||||
postgresSqlState,
|
||||
rollbackPostgresDefinitionTransaction,
|
||||
} from '../repository/definitionRepositorySupport';
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
|
||||
const IDENTITY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
const TERMINAL_RUN_STATUSES = new Set([
|
||||
'succeeded',
|
||||
'failed',
|
||||
'cancelled',
|
||||
'timed_out',
|
||||
]);
|
||||
const BARRIER_SELECT = `
|
||||
barrier.barrier_json AS "barrierJson",
|
||||
barrier.start_id AS "storedStartId",
|
||||
barrier.project_id AS "storedProjectId",
|
||||
barrier.run_id AS "storedRunId",
|
||||
barrier.step_run_id AS "storedStepRunId",
|
||||
barrier.started_step_run_version AS "storedStepRunVersion",
|
||||
barrier.step_run_mutation_id AS "storedMutationId",
|
||||
barrier.run_event_id AS "storedRunEventId",
|
||||
barrier.trace_id AS "storedTraceId",
|
||||
barrier.span_id AS "storedSpanId",
|
||||
barrier.audit_event_id::text AS "storedAuditEventId",
|
||||
barrier.command_digest AS "storedCommandDigest",
|
||||
barrier.barrier_digest AS "storedBarrierDigest",
|
||||
barrier.started_at_ms AS "storedStartedAtMs",
|
||||
mutation.mutation_digest AS "storedMutationDigest",
|
||||
mutation.step_run_digest AS "storedStartedStepRunDigest",
|
||||
trace.trace_digest AS "storedTraceDigest",
|
||||
receipt.receipt_digest AS "storedAuditReceiptDigest",
|
||||
artifact_binding.project_id AS "storedArtifactProjectId",
|
||||
artifact_binding.action_ref AS "storedArtifactActionRef",
|
||||
artifact_binding.input_artifact_id AS "storedInputArtifactId",
|
||||
artifact_binding.input_artifact_digest AS "storedInputArtifactDigest",
|
||||
artifact_binding.input_digest AS "storedInputDigest",
|
||||
artifact_binding.preview_artifact_id AS "storedPreviewArtifactId",
|
||||
artifact_binding.preview_artifact_digest AS "storedPreviewArtifactDigest",
|
||||
artifact_binding.action_digest AS "storedArtifactActionDigest",
|
||||
artifact_binding.preview_digest AS "storedPreviewDigest",
|
||||
artifact_binding.redaction_contract_digest
|
||||
AS "storedArtifactRedactionContractDigest",
|
||||
artifact_binding.bound_at_ms AS "storedArtifactBoundAtMs"
|
||||
`;
|
||||
|
||||
function unavailable(
|
||||
cause?: unknown,
|
||||
): ToolExecutionStartBarrierUnavailableError {
|
||||
return new ToolExecutionStartBarrierUnavailableError({
|
||||
cause: cause instanceof Error ? cause : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function requiredText(row: Row, key: string): string {
|
||||
return postgresRequiredString(row[key], unavailable);
|
||||
}
|
||||
|
||||
function requiredInteger(row: Row, key: string): number {
|
||||
const value = postgresRequiredInteger(row[key], unavailable);
|
||||
if (value < 0) throw unavailable();
|
||||
return value;
|
||||
}
|
||||
|
||||
function identity(value: unknown, label: string): string {
|
||||
if (typeof value !== 'string' || !IDENTITY_PATTERN.test(value)) {
|
||||
throw new InvalidToolExecutionStartBarrierError(`${label} is invalid`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function version(value: unknown): number {
|
||||
if (
|
||||
!Number.isSafeInteger(value) ||
|
||||
(value as number) < 2 ||
|
||||
(value as number) > 2_147_483_647
|
||||
) {
|
||||
throw new InvalidToolExecutionStartBarrierError(
|
||||
'started StepRun version is invalid',
|
||||
);
|
||||
}
|
||||
return value as number;
|
||||
}
|
||||
|
||||
function barrierFromRow(
|
||||
row: Row,
|
||||
): Readonly<ToolExecutionStartBarrierRecord> {
|
||||
let barrier: Readonly<ToolExecutionStartBarrierRecord>;
|
||||
try {
|
||||
barrier = normalizeToolExecutionStartBarrierRecord(
|
||||
postgresRequiredJsonObject(
|
||||
row.barrierJson,
|
||||
unavailable,
|
||||
) as unknown as ToolExecutionStartBarrierRecord,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof ToolExecutionStartBarrierUnavailableError) {
|
||||
throw error;
|
||||
}
|
||||
throw unavailable(error);
|
||||
}
|
||||
if (
|
||||
barrier.startId !== requiredText(row, 'storedStartId') ||
|
||||
barrier.projectId !== requiredText(row, 'storedProjectId') ||
|
||||
barrier.runId !== requiredText(row, 'storedRunId') ||
|
||||
barrier.stepRunId !== requiredText(row, 'storedStepRunId') ||
|
||||
barrier.startedStepRunVersion !==
|
||||
requiredInteger(row, 'storedStepRunVersion') ||
|
||||
barrier.stepRunMutationId !== requiredText(row, 'storedMutationId') ||
|
||||
barrier.runEventId !== requiredText(row, 'storedRunEventId') ||
|
||||
barrier.traceId !== requiredText(row, 'storedTraceId') ||
|
||||
barrier.spanId !== requiredText(row, 'storedSpanId') ||
|
||||
barrier.auditEventId !== requiredText(row, 'storedAuditEventId') ||
|
||||
barrier.commandDigest !== requiredText(row, 'storedCommandDigest') ||
|
||||
barrier.barrierDigest !== requiredText(row, 'storedBarrierDigest') ||
|
||||
barrier.startedAtMs !== requiredInteger(row, 'storedStartedAtMs') ||
|
||||
barrier.stepRunMutationDigest !==
|
||||
requiredText(row, 'storedMutationDigest') ||
|
||||
barrier.startedStepRunDigest !==
|
||||
requiredText(row, 'storedStartedStepRunDigest') ||
|
||||
barrier.traceDigest !== requiredText(row, 'storedTraceDigest') ||
|
||||
barrier.auditReceiptDigest !==
|
||||
requiredText(row, 'storedAuditReceiptDigest') ||
|
||||
barrier.projectId !== requiredText(row, 'storedArtifactProjectId') ||
|
||||
barrier.actionRef !== requiredText(row, 'storedArtifactActionRef') ||
|
||||
barrier.invocationArtifact.artifactId !==
|
||||
requiredText(row, 'storedInputArtifactId') ||
|
||||
barrier.invocationArtifact.artifactDigest !==
|
||||
requiredText(row, 'storedInputArtifactDigest') ||
|
||||
barrier.invocationArtifact.inputDigest !==
|
||||
requiredText(row, 'storedInputDigest') ||
|
||||
barrier.previewArtifact.artifactId !==
|
||||
requiredText(row, 'storedPreviewArtifactId') ||
|
||||
barrier.previewArtifact.artifactDigest !==
|
||||
requiredText(row, 'storedPreviewArtifactDigest') ||
|
||||
barrier.previewArtifact.actionDigest !==
|
||||
requiredText(row, 'storedArtifactActionDigest') ||
|
||||
barrier.previewArtifact.previewDigest !==
|
||||
requiredText(row, 'storedPreviewDigest') ||
|
||||
barrier.previewArtifact.redactionContractDigest !==
|
||||
requiredText(row, 'storedArtifactRedactionContractDigest') ||
|
||||
barrier.startedAtMs !== requiredInteger(row, 'storedArtifactBoundAtMs')
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
return barrier;
|
||||
}
|
||||
|
||||
function constraintError(error: unknown): boolean {
|
||||
const state = postgresSqlState(error);
|
||||
return state === '23503' || state === '23505' || state === '23514';
|
||||
}
|
||||
|
||||
function mapStorageError(error: unknown): Error {
|
||||
if (
|
||||
error instanceof InvalidToolExecutionStartBarrierError ||
|
||||
error instanceof ToolExecutionStartBarrierConflictError ||
|
||||
error instanceof ToolExecutionStartBarrierUnavailableError
|
||||
) {
|
||||
return error;
|
||||
}
|
||||
return constraintError(error)
|
||||
? new ToolExecutionStartBarrierConflictError()
|
||||
: unavailable(error);
|
||||
}
|
||||
|
||||
async function findRows(
|
||||
queryable: PostgresQueryable,
|
||||
where: string,
|
||||
values: readonly unknown[],
|
||||
): Promise<readonly Row[]> {
|
||||
const result = await queryable.query<Row>(
|
||||
`SELECT ${BARRIER_SELECT}
|
||||
FROM "ql3"."tool_execution_start_barriers" AS barrier
|
||||
JOIN "ql3"."step_run_mutations" AS mutation
|
||||
ON mutation.mutation_id = barrier.step_run_mutation_id
|
||||
JOIN "ql3"."tool_execution_trace_anchors" AS trace
|
||||
ON trace.trace_id = barrier.trace_id
|
||||
AND trace.span_id = barrier.span_id
|
||||
JOIN "ql3"."tool_execution_audit_receipts" AS receipt
|
||||
ON receipt.event_id = barrier.audit_event_id
|
||||
LEFT JOIN "ql3"."tool_execution_start_artifact_bindings"
|
||||
AS artifact_binding
|
||||
ON artifact_binding.start_id = barrier.start_id
|
||||
WHERE ${where}
|
||||
LIMIT 2`,
|
||||
values,
|
||||
);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
export class PostgresToolExecutionStartBarrierRepository
|
||||
implements ToolExecutionStartBarrierRepository
|
||||
{
|
||||
constructor(private readonly pool: PostgresPool) {
|
||||
if (
|
||||
!pool ||
|
||||
typeof pool.query !== 'function' ||
|
||||
typeof pool.connect !== 'function'
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
async findByStartId(
|
||||
startIdValue: string,
|
||||
): Promise<Readonly<ToolExecutionStartBarrierRecord> | null> {
|
||||
const startId = identity(startIdValue, 'start id');
|
||||
try {
|
||||
const rows = await findRows(
|
||||
this.pool,
|
||||
'barrier.start_id = $1',
|
||||
[startId],
|
||||
);
|
||||
if (rows.length > 1) throw unavailable();
|
||||
return rows[0] ? barrierFromRow(rows[0]) : null;
|
||||
} catch (error) {
|
||||
throw mapStorageError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async findByStepRun(
|
||||
runIdValue: string,
|
||||
stepRunIdValue: string,
|
||||
startedStepRunVersionValue: number,
|
||||
): Promise<Readonly<ToolExecutionStartBarrierRecord> | null> {
|
||||
const runId = identity(runIdValue, 'Run id');
|
||||
const stepRunId = identity(stepRunIdValue, 'StepRun id');
|
||||
const startedStepRunVersion = version(startedStepRunVersionValue);
|
||||
try {
|
||||
const rows = await findRows(
|
||||
this.pool,
|
||||
`barrier.run_id = $1 AND barrier.step_run_id = $2
|
||||
AND barrier.started_step_run_version = $3`,
|
||||
[runId, stepRunId, startedStepRunVersion],
|
||||
);
|
||||
if (rows.length > 1) throw unavailable();
|
||||
return rows[0] ? barrierFromRow(rows[0]) : null;
|
||||
} catch (error) {
|
||||
throw mapStorageError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async prepare(
|
||||
commandValue: ToolExecutionStartCommand,
|
||||
): Promise<Readonly<PrepareToolExecutionStartResult>> {
|
||||
const command = normalizeToolExecutionStartCommand(commandValue);
|
||||
const barrier = toolExecutionStartBarrierRecord(command);
|
||||
for (
|
||||
let attempt = 0;
|
||||
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
|
||||
attempt += 1
|
||||
) {
|
||||
let client: PostgresClient;
|
||||
try {
|
||||
client = await this.pool.connect();
|
||||
} catch (error) {
|
||||
throw unavailable(error);
|
||||
}
|
||||
let began = false;
|
||||
try {
|
||||
await configurePostgresDefinitionTransaction(client);
|
||||
began = true;
|
||||
const existing = await findRows(
|
||||
client,
|
||||
`barrier.start_id = $1
|
||||
OR barrier.step_run_mutation_id = $2
|
||||
OR barrier.run_event_id = $3
|
||||
OR (barrier.trace_id = $4 AND barrier.span_id = $5)
|
||||
OR barrier.audit_event_id = $6::uuid
|
||||
OR (
|
||||
barrier.run_id = $7 AND barrier.step_run_id = $8
|
||||
AND barrier.started_step_run_version = $9
|
||||
)`,
|
||||
[
|
||||
barrier.startId,
|
||||
barrier.stepRunMutationId,
|
||||
barrier.runEventId,
|
||||
barrier.traceId,
|
||||
barrier.spanId,
|
||||
barrier.auditEventId,
|
||||
barrier.runId,
|
||||
barrier.stepRunId,
|
||||
barrier.startedStepRunVersion,
|
||||
],
|
||||
);
|
||||
if (existing.length > 1) {
|
||||
throw new ToolExecutionStartBarrierConflictError();
|
||||
}
|
||||
if (existing[0]) {
|
||||
const stored = barrierFromRow(existing[0]);
|
||||
if (JSON.stringify(stored) !== JSON.stringify(barrier)) {
|
||||
throw new ToolExecutionStartBarrierConflictError();
|
||||
}
|
||||
await client.query('COMMIT');
|
||||
began = false;
|
||||
return Object.freeze({ status: 'existing', barrier: stored });
|
||||
}
|
||||
|
||||
const mutation = command.stepRunMutation;
|
||||
const current = await client.query<Row>(
|
||||
`SELECT
|
||||
step.kind AS "stepKind", step.status AS "stepStatus",
|
||||
step.version AS "stepVersion",
|
||||
step.step_run_digest AS "stepDigest",
|
||||
step.definition_ref AS "definitionRef",
|
||||
step.definition_digest AS "definitionDigest",
|
||||
run.project_id AS "projectId", run.status AS "runStatus",
|
||||
run.version AS "runVersion",
|
||||
run.event_sequence AS "runEventSequence"
|
||||
FROM "ql3"."step_runs" AS step
|
||||
JOIN "ql3"."runs" AS run ON run.id = step.run_id
|
||||
WHERE step.id = $1 AND step.run_id = $2
|
||||
LIMIT 2
|
||||
FOR UPDATE OF step, run`,
|
||||
[barrier.stepRunId, barrier.runId],
|
||||
);
|
||||
const row = current.rows[0];
|
||||
if (
|
||||
current.rows.length !== 1 ||
|
||||
!row ||
|
||||
requiredText(row, 'stepKind') !== 'tool' ||
|
||||
requiredText(row, 'stepStatus') !== mutation.previousStatus ||
|
||||
requiredInteger(row, 'stepVersion') !==
|
||||
mutation.expectedStepRunVersion ||
|
||||
requiredText(row, 'stepDigest') !==
|
||||
mutation.expectedStepRunDigest ||
|
||||
requiredText(row, 'definitionRef') !==
|
||||
mutation.stepRun.definitionRef ||
|
||||
requiredText(row, 'definitionDigest') !==
|
||||
mutation.stepRun.definitionDigest ||
|
||||
requiredText(row, 'projectId') !== barrier.projectId ||
|
||||
requiredInteger(row, 'runVersion') !==
|
||||
mutation.expectedRunVersion ||
|
||||
requiredInteger(row, 'runEventSequence') !==
|
||||
mutation.expectedRunEventSequence ||
|
||||
TERMINAL_RUN_STATUSES.has(requiredText(row, 'runStatus'))
|
||||
) {
|
||||
throw new ToolExecutionStartBarrierConflictError();
|
||||
}
|
||||
const quarantineFence = await client.query<Row>(
|
||||
`SELECT "ql3"."plugin_package_tool_start_allowed"(
|
||||
$1::varchar, $2::varchar, $3::char(64)
|
||||
) AS "allowed"`,
|
||||
[
|
||||
barrier.projectId,
|
||||
mutation.stepRun.definitionRef,
|
||||
mutation.stepRun.definitionDigest,
|
||||
],
|
||||
);
|
||||
if (
|
||||
quarantineFence.rows.length !== 1 ||
|
||||
quarantineFence.rows[0]?.allowed !== true
|
||||
) {
|
||||
throw new ToolExecutionStartBarrierConflictError();
|
||||
}
|
||||
|
||||
const evidence = command.evidence;
|
||||
const audit = evidence.audit;
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."security_audit_events" (
|
||||
event_id, request_id, operation_id, project_id, subject_type,
|
||||
subject_id, authentication_id, outcome, reasons,
|
||||
project_version, binding_version, occurred_at_ms
|
||||
) VALUES (
|
||||
$1::uuid, $2, $3, $4, $5, $6, $7, $8, $9::jsonb,
|
||||
$10, $11, $12
|
||||
)`,
|
||||
[
|
||||
audit.eventId,
|
||||
audit.requestId,
|
||||
audit.operationId,
|
||||
audit.projectId,
|
||||
audit.subject?.type ?? null,
|
||||
audit.subject?.id ?? null,
|
||||
audit.authenticationId,
|
||||
audit.outcome,
|
||||
JSON.stringify(audit.reasons),
|
||||
audit.fence?.projectVersion ?? null,
|
||||
audit.fence?.bindingVersion ?? null,
|
||||
audit.occurredAtMs,
|
||||
],
|
||||
);
|
||||
const trace = evidence.trace;
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."tool_execution_trace_anchors" (
|
||||
trace_id, span_id, parent_span_id, project_id, run_id,
|
||||
step_run_id, invocation_plan_digest, binding_digest,
|
||||
adapter_digest, redaction_contract_digest,
|
||||
audit_contract_digest, created_at_ms, trace_digest, trace_json
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
|
||||
$14::jsonb
|
||||
)`,
|
||||
[
|
||||
trace.traceId,
|
||||
trace.spanId,
|
||||
trace.parentSpanId,
|
||||
trace.projectId,
|
||||
trace.runId,
|
||||
trace.stepRunId,
|
||||
trace.invocationPlanDigest,
|
||||
trace.bindingDigest,
|
||||
trace.adapterDigest,
|
||||
trace.redactionContractDigest,
|
||||
trace.auditContractDigest,
|
||||
trace.createdAtMs,
|
||||
trace.traceDigest,
|
||||
JSON.stringify(trace),
|
||||
],
|
||||
);
|
||||
const receipt = evidence.receipt;
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."tool_execution_audit_receipts" (
|
||||
event_id, project_id, run_id, step_run_id, trace_id, span_id,
|
||||
trace_digest, invocation_plan_digest, binding_digest,
|
||||
audit_record_digest, created_at_ms, receipt_digest,
|
||||
audit_json, receipt_json
|
||||
) VALUES (
|
||||
$1::uuid, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12,
|
||||
$13::jsonb, $14::jsonb
|
||||
)`,
|
||||
[
|
||||
receipt.eventId,
|
||||
receipt.projectId,
|
||||
receipt.runId,
|
||||
receipt.stepRunId,
|
||||
receipt.traceId,
|
||||
receipt.spanId,
|
||||
receipt.traceDigest,
|
||||
receipt.invocationPlanDigest,
|
||||
receipt.bindingDigest,
|
||||
receipt.auditRecordDigest,
|
||||
receipt.createdAtMs,
|
||||
receipt.receiptDigest,
|
||||
JSON.stringify(evidence.audit),
|
||||
JSON.stringify(receipt),
|
||||
],
|
||||
);
|
||||
const step = mutation.stepRun;
|
||||
const updatedStep = await client.query(
|
||||
`UPDATE "ql3"."step_runs"
|
||||
SET status = $1, version = $2, attempt_count = $3,
|
||||
output_ref = $4, approval_request_id = $5, ready_at_ms = $6,
|
||||
started_at_ms = $7, finished_at_ms = $8, result_code = $9,
|
||||
error_summary = $10, updated_at_ms = $11,
|
||||
last_mutation_id = $12, step_run_digest = $13,
|
||||
step_run_json = $14::jsonb
|
||||
WHERE id = $15 AND run_id = $16 AND version = $17
|
||||
AND step_run_digest = $18 AND status = $19`,
|
||||
[
|
||||
step.status,
|
||||
step.version,
|
||||
step.attemptCount,
|
||||
step.outputRef,
|
||||
step.approvalRequestId,
|
||||
step.readyAtMs,
|
||||
step.startedAtMs,
|
||||
step.finishedAtMs,
|
||||
step.resultCode,
|
||||
step.errorSummary,
|
||||
step.updatedAtMs,
|
||||
step.lastMutationId,
|
||||
step.stepRunDigest,
|
||||
JSON.stringify(step),
|
||||
step.id,
|
||||
step.runId,
|
||||
mutation.expectedStepRunVersion,
|
||||
mutation.expectedStepRunDigest,
|
||||
mutation.previousStatus,
|
||||
],
|
||||
);
|
||||
if (updatedStep.rowCount !== 1) {
|
||||
throw new ToolExecutionStartBarrierConflictError();
|
||||
}
|
||||
const updatedRun = await client.query(
|
||||
`UPDATE "ql3"."runs"
|
||||
SET version = version + 1, event_sequence = event_sequence + 1
|
||||
WHERE id = $1 AND version = $2 AND event_sequence = $3`,
|
||||
[
|
||||
mutation.runId,
|
||||
mutation.expectedRunVersion,
|
||||
mutation.expectedRunEventSequence,
|
||||
],
|
||||
);
|
||||
if (updatedRun.rowCount !== 1) {
|
||||
throw new ToolExecutionStartBarrierConflictError();
|
||||
}
|
||||
const event = mutation.event;
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."run_events" (
|
||||
id, run_id, sequence, type, dedupe_key, actor_type, actor_id,
|
||||
attempt_id, step_run_id, payload, created_at_ms
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, NULL, $8, $9::jsonb, $10
|
||||
)`,
|
||||
[
|
||||
event.id,
|
||||
event.runId,
|
||||
event.sequence,
|
||||
event.type,
|
||||
event.dedupeKey,
|
||||
event.actorType,
|
||||
event.actorId ?? null,
|
||||
step.id,
|
||||
JSON.stringify(event.payload),
|
||||
event.createdAtMs,
|
||||
],
|
||||
);
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."step_run_mutations" (
|
||||
mutation_id, mutation_digest, run_id, step_run_id,
|
||||
step_run_digest, event_id, event_sequence, run_version,
|
||||
step_run_json, committed_at_ms
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb,
|
||||
floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
)`,
|
||||
[
|
||||
mutation.mutationId,
|
||||
mutation.mutationDigest,
|
||||
mutation.runId,
|
||||
step.id,
|
||||
step.stepRunDigest,
|
||||
event.id,
|
||||
event.sequence,
|
||||
mutation.expectedRunVersion + 1,
|
||||
JSON.stringify(step),
|
||||
],
|
||||
);
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."tool_execution_start_barriers" (
|
||||
start_id, project_id, run_id, step_run_id,
|
||||
started_step_run_version, step_run_mutation_id, run_event_id,
|
||||
trace_id, span_id, audit_event_id, command_digest,
|
||||
barrier_digest, started_at_ms, barrier_json
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10::uuid, $11, $12,
|
||||
$13, $14::jsonb
|
||||
)`,
|
||||
[
|
||||
barrier.startId,
|
||||
barrier.projectId,
|
||||
barrier.runId,
|
||||
barrier.stepRunId,
|
||||
barrier.startedStepRunVersion,
|
||||
barrier.stepRunMutationId,
|
||||
barrier.runEventId,
|
||||
barrier.traceId,
|
||||
barrier.spanId,
|
||||
barrier.auditEventId,
|
||||
barrier.commandDigest,
|
||||
barrier.barrierDigest,
|
||||
barrier.startedAtMs,
|
||||
JSON.stringify(barrier),
|
||||
],
|
||||
);
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."tool_execution_start_artifact_bindings" (
|
||||
start_id, project_id, action_ref,
|
||||
input_artifact_id, input_artifact_digest, input_digest,
|
||||
preview_artifact_id, preview_artifact_digest, action_digest,
|
||||
preview_digest, redaction_contract_digest, bound_at_ms
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
|
||||
)`,
|
||||
[
|
||||
barrier.startId,
|
||||
barrier.projectId,
|
||||
barrier.actionRef,
|
||||
barrier.invocationArtifact.artifactId,
|
||||
barrier.invocationArtifact.artifactDigest,
|
||||
barrier.invocationArtifact.inputDigest,
|
||||
barrier.previewArtifact.artifactId,
|
||||
barrier.previewArtifact.artifactDigest,
|
||||
barrier.previewArtifact.actionDigest,
|
||||
barrier.previewArtifact.previewDigest,
|
||||
barrier.previewArtifact.redactionContractDigest,
|
||||
barrier.startedAtMs,
|
||||
],
|
||||
);
|
||||
await client.query('COMMIT');
|
||||
began = false;
|
||||
return Object.freeze({ status: 'created', barrier });
|
||||
} catch (error) {
|
||||
if (began) await rollbackPostgresDefinitionTransaction(client);
|
||||
const state = postgresSqlState(error);
|
||||
if (
|
||||
((state &&
|
||||
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state)) ||
|
||||
state === '23505') &&
|
||||
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
throw mapStorageError(error);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
import type { PostgresClient, PostgresPool } from '@qinglong/runtime-core';
|
||||
import {
|
||||
InvalidToolInvocationArtifactError,
|
||||
MAX_TOOL_INVOCATION_INPUT_ARTIFACT_JSON_BYTES,
|
||||
MAX_TOOL_INVOCATION_PREVIEW_ARTIFACT_JSON_BYTES,
|
||||
ToolInvocationArtifactConflictError,
|
||||
ToolInvocationArtifactUnavailableError,
|
||||
normalizeToolInvocationInputArtifact,
|
||||
normalizeToolInvocationPreviewArtifact,
|
||||
type ToolInvocationArtifactRepository,
|
||||
type ToolInvocationInputArtifact,
|
||||
type ToolInvocationPreviewArtifact,
|
||||
} from '@qinglong/runtime-core/tool-invocation-artifact';
|
||||
|
||||
import {
|
||||
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
|
||||
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
|
||||
configurePostgresDefinitionTransaction,
|
||||
postgresRequiredInteger,
|
||||
postgresRequiredJsonObject,
|
||||
postgresRequiredString,
|
||||
postgresSqlState,
|
||||
rollbackPostgresDefinitionTransaction,
|
||||
} from '../repository/definitionRepositorySupport';
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
type Queryable = Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>;
|
||||
|
||||
const ARTIFACT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
|
||||
function unavailable(cause?: unknown): ToolInvocationArtifactUnavailableError {
|
||||
return new ToolInvocationArtifactUnavailableError({
|
||||
cause: cause instanceof Error ? cause : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function artifactId(value: unknown): string {
|
||||
if (typeof value !== 'string' || !ARTIFACT_ID_PATTERN.test(value)) {
|
||||
throw new InvalidToolInvocationArtifactError('artifact id is invalid');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function serialize(value: unknown, maximumBytes: number): string {
|
||||
const json = JSON.stringify(value);
|
||||
if (Buffer.byteLength(json, 'utf8') > maximumBytes) {
|
||||
throw new InvalidToolInvocationArtifactError(
|
||||
'durable Artifact JSON exceeds its budget',
|
||||
);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
function mappedError(error: unknown): Error {
|
||||
if (
|
||||
error instanceof InvalidToolInvocationArtifactError ||
|
||||
error instanceof ToolInvocationArtifactConflictError ||
|
||||
error instanceof ToolInvocationArtifactUnavailableError
|
||||
) {
|
||||
return error;
|
||||
}
|
||||
const state = postgresSqlState(error);
|
||||
if (state === '23503' || state === '23505' || state === '23514') {
|
||||
return new ToolInvocationArtifactConflictError();
|
||||
}
|
||||
return unavailable(error);
|
||||
}
|
||||
|
||||
function requiredString(value: unknown): string {
|
||||
return postgresRequiredString(value, unavailable);
|
||||
}
|
||||
|
||||
function requiredInteger(value: unknown): number {
|
||||
return postgresRequiredInteger(value, unavailable);
|
||||
}
|
||||
|
||||
export class PostgresToolInvocationArtifactRepository
|
||||
implements ToolInvocationArtifactRepository
|
||||
{
|
||||
constructor(private readonly pool: PostgresPool) {
|
||||
if (
|
||||
!pool ||
|
||||
typeof pool.query !== 'function' ||
|
||||
typeof pool.connect !== 'function'
|
||||
) {
|
||||
throw new TypeError(
|
||||
'PostgreSQL Tool invocation Artifact repository options are invalid',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private parseInput(row: Row): Readonly<ToolInvocationInputArtifact> {
|
||||
try {
|
||||
const artifact = normalizeToolInvocationInputArtifact(
|
||||
postgresRequiredJsonObject(
|
||||
row.artifactJson,
|
||||
unavailable,
|
||||
) as unknown as ToolInvocationInputArtifact,
|
||||
);
|
||||
if (
|
||||
artifact.artifactId !== requiredString(row.artifactId) ||
|
||||
artifact.projectId !== requiredString(row.projectId) ||
|
||||
artifact.actionRef !== requiredString(row.actionRef) ||
|
||||
artifact.inputDigest !== requiredString(row.inputDigest) ||
|
||||
artifact.invocationActionDigest !==
|
||||
requiredString(row.invocationActionDigest) ||
|
||||
artifact.artifactDigest !== requiredString(row.artifactDigest) ||
|
||||
artifact.keyId !== requiredString(row.keyId) ||
|
||||
artifact.algorithm !== requiredString(row.algorithm) ||
|
||||
artifact.plaintextBytes !== requiredInteger(row.plaintextBytes) ||
|
||||
artifact.sealedAtMs !== requiredInteger(row.sealedAtMs) ||
|
||||
Buffer.byteLength(JSON.stringify(artifact), 'utf8') >
|
||||
MAX_TOOL_INVOCATION_INPUT_ARTIFACT_JSON_BYTES
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
return artifact;
|
||||
} catch (error) {
|
||||
if (error instanceof ToolInvocationArtifactUnavailableError) throw error;
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
private parsePreview(
|
||||
row: Row,
|
||||
): Readonly<ToolInvocationPreviewArtifact> {
|
||||
try {
|
||||
const artifact = normalizeToolInvocationPreviewArtifact(
|
||||
postgresRequiredJsonObject(
|
||||
row.artifactJson,
|
||||
unavailable,
|
||||
) as unknown as ToolInvocationPreviewArtifact,
|
||||
);
|
||||
if (
|
||||
artifact.artifactId !== requiredString(row.artifactId) ||
|
||||
artifact.projectId !== requiredString(row.projectId) ||
|
||||
artifact.actionRef !== requiredString(row.actionRef) ||
|
||||
artifact.actionDigest !== requiredString(row.actionDigest) ||
|
||||
artifact.previewDigest !== requiredString(row.previewDigest) ||
|
||||
artifact.redactionContractDigest !==
|
||||
requiredString(row.redactionContractDigest) ||
|
||||
artifact.artifactDigest !== requiredString(row.artifactDigest) ||
|
||||
artifact.byteLength !== requiredInteger(row.byteLength) ||
|
||||
artifact.sealedAtMs !== requiredInteger(row.sealedAtMs) ||
|
||||
Buffer.byteLength(JSON.stringify(artifact), 'utf8') >
|
||||
MAX_TOOL_INVOCATION_PREVIEW_ARTIFACT_JSON_BYTES
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
return artifact;
|
||||
} catch (error) {
|
||||
if (error instanceof ToolInvocationArtifactUnavailableError) throw error;
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
private async storedInput(
|
||||
queryable: Queryable,
|
||||
id: string,
|
||||
): Promise<Readonly<ToolInvocationInputArtifact> | null> {
|
||||
const result = await queryable.query<Row>(
|
||||
`SELECT
|
||||
artifact_id AS "artifactId",
|
||||
project_id AS "projectId",
|
||||
action_ref AS "actionRef",
|
||||
input_digest AS "inputDigest",
|
||||
invocation_action_digest AS "invocationActionDigest",
|
||||
artifact_digest AS "artifactDigest",
|
||||
key_id AS "keyId",
|
||||
algorithm,
|
||||
plaintext_bytes AS "plaintextBytes",
|
||||
sealed_at_ms AS "sealedAtMs",
|
||||
artifact_json AS "artifactJson"
|
||||
FROM "ql3"."tool_invocation_input_artifacts"
|
||||
WHERE artifact_id = $1
|
||||
LIMIT 2`,
|
||||
[id],
|
||||
);
|
||||
if (result.rows.length > 1) throw unavailable();
|
||||
return result.rows[0] ? this.parseInput(result.rows[0]) : null;
|
||||
}
|
||||
|
||||
private async storedPreview(
|
||||
queryable: Queryable,
|
||||
id: string,
|
||||
): Promise<Readonly<ToolInvocationPreviewArtifact> | null> {
|
||||
const result = await queryable.query<Row>(
|
||||
`SELECT
|
||||
artifact_id AS "artifactId",
|
||||
project_id AS "projectId",
|
||||
action_ref AS "actionRef",
|
||||
action_digest AS "actionDigest",
|
||||
preview_digest AS "previewDigest",
|
||||
redaction_contract_digest AS "redactionContractDigest",
|
||||
artifact_digest AS "artifactDigest",
|
||||
byte_length AS "byteLength",
|
||||
sealed_at_ms AS "sealedAtMs",
|
||||
artifact_json AS "artifactJson"
|
||||
FROM "ql3"."tool_invocation_preview_artifacts"
|
||||
WHERE artifact_id = $1
|
||||
LIMIT 2`,
|
||||
[id],
|
||||
);
|
||||
if (result.rows.length > 1) throw unavailable();
|
||||
return result.rows[0] ? this.parsePreview(result.rows[0]) : null;
|
||||
}
|
||||
|
||||
async put(
|
||||
inputValue: ToolInvocationInputArtifact,
|
||||
previewValue: ToolInvocationPreviewArtifact,
|
||||
): Promise<Readonly<{ status: 'inserted' | 'existing' }>> {
|
||||
const input = normalizeToolInvocationInputArtifact(inputValue);
|
||||
const preview = normalizeToolInvocationPreviewArtifact(previewValue);
|
||||
if (
|
||||
input.projectId !== preview.projectId ||
|
||||
input.actionRef !== preview.actionRef ||
|
||||
input.sealedAtMs !== preview.sealedAtMs
|
||||
) {
|
||||
throw new ToolInvocationArtifactConflictError();
|
||||
}
|
||||
serialize(input, MAX_TOOL_INVOCATION_INPUT_ARTIFACT_JSON_BYTES);
|
||||
serialize(preview, MAX_TOOL_INVOCATION_PREVIEW_ARTIFACT_JSON_BYTES);
|
||||
for (
|
||||
let attempt = 1;
|
||||
attempt <= POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
|
||||
attempt += 1
|
||||
) {
|
||||
let client: PostgresClient;
|
||||
try {
|
||||
client = await this.pool.connect();
|
||||
} catch {
|
||||
throw unavailable();
|
||||
}
|
||||
let began = false;
|
||||
try {
|
||||
await configurePostgresDefinitionTransaction(client);
|
||||
began = true;
|
||||
const storedInput = await this.storedInput(client, input.artifactId);
|
||||
const storedPreview = await this.storedPreview(
|
||||
client,
|
||||
preview.artifactId,
|
||||
);
|
||||
if (storedInput || storedPreview) {
|
||||
if (
|
||||
!storedInput ||
|
||||
!storedPreview ||
|
||||
JSON.stringify(storedInput) !== JSON.stringify(input) ||
|
||||
JSON.stringify(storedPreview) !== JSON.stringify(preview)
|
||||
) {
|
||||
throw new ToolInvocationArtifactConflictError();
|
||||
}
|
||||
await client.query('COMMIT');
|
||||
began = false;
|
||||
return Object.freeze({ status: 'existing' });
|
||||
}
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."tool_invocation_input_artifacts" (
|
||||
artifact_id, project_id, action_ref, input_digest,
|
||||
invocation_action_digest, artifact_digest, key_id, algorithm,
|
||||
plaintext_bytes, sealed_at_ms, artifact_json
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb
|
||||
)`,
|
||||
[
|
||||
input.artifactId,
|
||||
input.projectId,
|
||||
input.actionRef,
|
||||
input.inputDigest,
|
||||
input.invocationActionDigest,
|
||||
input.artifactDigest,
|
||||
input.keyId,
|
||||
input.algorithm,
|
||||
input.plaintextBytes,
|
||||
input.sealedAtMs,
|
||||
JSON.stringify(input),
|
||||
],
|
||||
);
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."tool_invocation_preview_artifacts" (
|
||||
artifact_id, project_id, action_ref, action_digest,
|
||||
preview_digest, redaction_contract_digest, artifact_digest,
|
||||
byte_length, sealed_at_ms, artifact_json
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb
|
||||
)`,
|
||||
[
|
||||
preview.artifactId,
|
||||
preview.projectId,
|
||||
preview.actionRef,
|
||||
preview.actionDigest,
|
||||
preview.previewDigest,
|
||||
preview.redactionContractDigest,
|
||||
preview.artifactDigest,
|
||||
preview.byteLength,
|
||||
preview.sealedAtMs,
|
||||
JSON.stringify(preview),
|
||||
],
|
||||
);
|
||||
await client.query('COMMIT');
|
||||
began = false;
|
||||
return Object.freeze({ status: 'inserted' });
|
||||
} catch (error) {
|
||||
if (began) await rollbackPostgresDefinitionTransaction(client);
|
||||
const state = postgresSqlState(error);
|
||||
if (
|
||||
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS &&
|
||||
state !== undefined &&
|
||||
(POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) ||
|
||||
state === '23505')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
throw mappedError(error);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
throw unavailable();
|
||||
}
|
||||
|
||||
async findInput(
|
||||
value: string,
|
||||
): Promise<Readonly<ToolInvocationInputArtifact> | null> {
|
||||
try {
|
||||
return await this.storedInput(this.pool, artifactId(value));
|
||||
} catch (error) {
|
||||
throw mappedError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async findPreview(
|
||||
value: string,
|
||||
): Promise<Readonly<ToolInvocationPreviewArtifact> | null> {
|
||||
try {
|
||||
return await this.storedPreview(this.pool, artifactId(value));
|
||||
} catch (error) {
|
||||
throw mappedError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
import type { PostgresClient, PostgresPool } from '@qinglong/runtime-core';
|
||||
import {
|
||||
InvalidToolResultKeyCatalogError,
|
||||
ToolResultKeyCatalogConflictError,
|
||||
ToolResultKeyCatalogUnavailableError,
|
||||
assertToolResultKeyCatalogTransition,
|
||||
normalizeToolResultKeyCatalogCommand,
|
||||
normalizeToolResultKeyCatalogRecord,
|
||||
type CommitToolResultKeyCatalogResult,
|
||||
type ToolResultKeyCatalogCommand,
|
||||
type ToolResultKeyCatalogReader,
|
||||
type ToolResultKeyCatalogRecord,
|
||||
type ToolResultKeyCatalogRepository,
|
||||
} from '@qinglong/runtime-core/tool-result-key-catalog';
|
||||
import {
|
||||
normalizeToolResultKeyRetirementReceipt,
|
||||
type ToolResultKeyRetirementReceipt,
|
||||
} from '@qinglong/runtime-core/tool-result-rekey';
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
|
||||
const AUTHORITY = 'trusted-tool-results';
|
||||
const CATALOG_TRANSACTION_LOCK = 'SELECT pg_advisory_xact_lock(190397473, 3)';
|
||||
|
||||
function unavailable(cause?: unknown): ToolResultKeyCatalogUnavailableError {
|
||||
return new ToolResultKeyCatalogUnavailableError({
|
||||
cause: cause instanceof Error ? cause : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function postgresConstraint(error: unknown): boolean {
|
||||
return (
|
||||
!!error &&
|
||||
typeof error === 'object' &&
|
||||
typeof (error as { code?: unknown }).code === 'string' &&
|
||||
['23503', '23505', '23514'].includes((error as { code: string }).code)
|
||||
);
|
||||
}
|
||||
|
||||
function mapStorageError(error: unknown): Error {
|
||||
if (
|
||||
error instanceof InvalidToolResultKeyCatalogError ||
|
||||
error instanceof ToolResultKeyCatalogConflictError ||
|
||||
error instanceof ToolResultKeyCatalogUnavailableError
|
||||
) {
|
||||
return error;
|
||||
}
|
||||
return postgresConstraint(error)
|
||||
? new ToolResultKeyCatalogConflictError()
|
||||
: unavailable(error);
|
||||
}
|
||||
|
||||
function requiredText(row: Row, key: string): string {
|
||||
const value = row[key];
|
||||
if (typeof value !== 'string') throw unavailable();
|
||||
return value;
|
||||
}
|
||||
|
||||
function requiredNullableText(row: Row, key: string): string | null {
|
||||
const value = row[key];
|
||||
if (value !== null && typeof value !== 'string') throw unavailable();
|
||||
return value;
|
||||
}
|
||||
|
||||
function requiredInteger(row: Row, key: string): number {
|
||||
const value = Number(row[key]);
|
||||
if (!Number.isSafeInteger(value) || value < 0) throw unavailable();
|
||||
return value;
|
||||
}
|
||||
|
||||
function recordFromRow(row: Row): Readonly<ToolResultKeyCatalogRecord> {
|
||||
const value = row.catalogJson;
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw unavailable();
|
||||
}
|
||||
let catalog: Readonly<ToolResultKeyCatalogRecord>;
|
||||
try {
|
||||
catalog = normalizeToolResultKeyCatalogRecord(
|
||||
value as ToolResultKeyCatalogRecord,
|
||||
);
|
||||
const { committedAtMs: _committedAtMs, ...next } = catalog;
|
||||
normalizeToolResultKeyCatalogCommand({
|
||||
schema: 'qinglong/tool-result-key-catalog-command@v1',
|
||||
expectedGeneration: catalog.generation - 1,
|
||||
expectedCatalogDigest: catalog.previousCatalogDigest,
|
||||
next,
|
||||
commandDigest: requiredText(row, 'commandDigest'),
|
||||
});
|
||||
} catch {
|
||||
throw unavailable();
|
||||
}
|
||||
if (
|
||||
catalog.generation !== requiredInteger(row, 'generation') ||
|
||||
catalog.previousCatalogDigest !==
|
||||
requiredNullableText(row, 'previousCatalogDigest') ||
|
||||
catalog.activeKeyId !== requiredNullableText(row, 'activeKeyId') ||
|
||||
catalog.mutationKind !== requiredText(row, 'mutationKind') ||
|
||||
catalog.mutationId !== requiredText(row, 'mutationId') ||
|
||||
catalog.catalogDigest !== requiredText(row, 'catalogDigest') ||
|
||||
catalog.committedAtMs !== requiredInteger(row, 'committedAtMs')
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
return catalog;
|
||||
}
|
||||
|
||||
async function rollback(client: PostgresClient): Promise<void> {
|
||||
await client.query('ROLLBACK').catch(() => undefined);
|
||||
}
|
||||
|
||||
export class PostgresToolResultKeyCatalogReader
|
||||
implements ToolResultKeyCatalogReader
|
||||
{
|
||||
constructor(protected readonly pool: PostgresPool) {
|
||||
if (
|
||||
!pool ||
|
||||
typeof pool.query !== 'function' ||
|
||||
typeof pool.connect !== 'function'
|
||||
) {
|
||||
throw new TypeError('PostgreSQL Tool result key catalog pool is invalid');
|
||||
}
|
||||
}
|
||||
|
||||
protected async rows(
|
||||
client: Pick<PostgresClient, 'query'> | PostgresPool,
|
||||
suffix = '',
|
||||
values: readonly unknown[] = [],
|
||||
lock = false,
|
||||
): Promise<readonly Row[]> {
|
||||
const result = await client.query<Row>(
|
||||
`SELECT
|
||||
generation,
|
||||
previous_catalog_digest AS "previousCatalogDigest",
|
||||
active_key_id AS "activeKeyId",
|
||||
mutation_kind AS "mutationKind",
|
||||
mutation_id AS "mutationId",
|
||||
catalog_digest AS "catalogDigest",
|
||||
command_digest AS "commandDigest",
|
||||
committed_at_ms AS "committedAtMs",
|
||||
catalog_json AS "catalogJson"
|
||||
FROM "ql3"."tool_result_key_catalog_generations"
|
||||
WHERE authority = $1 ${suffix}
|
||||
ORDER BY generation DESC
|
||||
LIMIT 2${lock ? ' FOR UPDATE' : ''}`,
|
||||
[AUTHORITY, ...values],
|
||||
);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
async findCurrent(): Promise<Readonly<ToolResultKeyCatalogRecord> | null> {
|
||||
try {
|
||||
const rows = await this.rows(this.pool);
|
||||
if (
|
||||
rows.length > 1 &&
|
||||
requiredInteger(rows[0]!, 'generation') ===
|
||||
requiredInteger(rows[1]!, 'generation')
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
return rows[0] ? recordFromRow(rows[0]) : null;
|
||||
} catch (error) {
|
||||
throw mapStorageError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class PostgresToolResultKeyCatalogRepository
|
||||
extends PostgresToolResultKeyCatalogReader
|
||||
implements ToolResultKeyCatalogRepository
|
||||
{
|
||||
async append(
|
||||
commandValue: Readonly<ToolResultKeyCatalogCommand>,
|
||||
): Promise<Readonly<CommitToolResultKeyCatalogResult>> {
|
||||
const command = normalizeToolResultKeyCatalogCommand(commandValue);
|
||||
let client: PostgresClient;
|
||||
try {
|
||||
client = await this.pool.connect();
|
||||
} catch (error) {
|
||||
throw unavailable(error);
|
||||
}
|
||||
try {
|
||||
await client.query('BEGIN ISOLATION LEVEL SERIALIZABLE');
|
||||
await client.query(CATALOG_TRANSACTION_LOCK);
|
||||
const replayRows = await this.rows(
|
||||
client,
|
||||
'AND (mutation_id = $2 OR generation = $3 OR catalog_digest = $4)',
|
||||
[
|
||||
command.next.mutationId,
|
||||
command.next.generation,
|
||||
command.next.catalogDigest,
|
||||
],
|
||||
);
|
||||
if (replayRows.length > 1) {
|
||||
throw new ToolResultKeyCatalogConflictError();
|
||||
}
|
||||
if (replayRows[0]) {
|
||||
const stored = recordFromRow(replayRows[0]);
|
||||
const { committedAtMs: _storedTime, ...storedSnapshot } = stored;
|
||||
if (
|
||||
requiredText(replayRows[0], 'commandDigest') !==
|
||||
command.commandDigest ||
|
||||
JSON.stringify(storedSnapshot) !== JSON.stringify(command.next)
|
||||
) {
|
||||
throw new ToolResultKeyCatalogConflictError();
|
||||
}
|
||||
await client.query('COMMIT');
|
||||
return Object.freeze({
|
||||
status: 'existing' as const,
|
||||
catalog: stored,
|
||||
});
|
||||
}
|
||||
|
||||
const currentRows = await this.rows(client);
|
||||
if (
|
||||
currentRows.length > 1 &&
|
||||
requiredInteger(currentRows[0]!, 'generation') ===
|
||||
requiredInteger(currentRows[1]!, 'generation')
|
||||
) {
|
||||
throw new ToolResultKeyCatalogConflictError();
|
||||
}
|
||||
const current = currentRows[0] ? recordFromRow(currentRows[0]) : null;
|
||||
assertToolResultKeyCatalogTransition(current, command);
|
||||
if (command.next.mutationKind === 'retire') {
|
||||
if (!current) throw new ToolResultKeyCatalogConflictError();
|
||||
const retired = command.next.keys.find(
|
||||
(entry) => entry.state === 'retired',
|
||||
);
|
||||
const previous = retired
|
||||
? current.keys.find((entry) => entry.keyId === retired.keyId)
|
||||
: null;
|
||||
if (
|
||||
!retired ||
|
||||
!previous ||
|
||||
retired.retirementReceiptDigest === null
|
||||
) {
|
||||
throw new ToolResultKeyCatalogConflictError();
|
||||
}
|
||||
const receiptResult = await client.query<Row>(
|
||||
`SELECT receipt_json AS "receiptJson"
|
||||
FROM "ql3"."tool_result_key_retirement_receipts"
|
||||
WHERE receipt_digest = $1`,
|
||||
[retired.retirementReceiptDigest],
|
||||
);
|
||||
let receipt: Readonly<ToolResultKeyRetirementReceipt>;
|
||||
try {
|
||||
if (receiptResult.rows.length !== 1) {
|
||||
throw new Error('retirement receipt count');
|
||||
}
|
||||
const receiptJson = receiptResult.rows[0]!.receiptJson;
|
||||
if (
|
||||
!receiptJson ||
|
||||
typeof receiptJson !== 'object' ||
|
||||
Array.isArray(receiptJson) ||
|
||||
Buffer.byteLength(JSON.stringify(receiptJson), 'utf8') >
|
||||
64 * 1024
|
||||
) {
|
||||
throw new Error('retirement receipt budget');
|
||||
}
|
||||
receipt = normalizeToolResultKeyRetirementReceipt(
|
||||
receiptJson as ToolResultKeyRetirementReceipt,
|
||||
);
|
||||
} catch {
|
||||
throw new ToolResultKeyCatalogConflictError();
|
||||
}
|
||||
if (
|
||||
receipt.receiptDigest !== retired.retirementReceiptDigest ||
|
||||
receipt.catalogGeneration !== current.generation ||
|
||||
receipt.catalogDigest !== current.catalogDigest ||
|
||||
receipt.keyId !== previous.keyId ||
|
||||
receipt.materialProof !== previous.materialProof
|
||||
) {
|
||||
throw new ToolResultKeyCatalogConflictError();
|
||||
}
|
||||
}
|
||||
const clock = await client.query<Row>(
|
||||
`SELECT floor(
|
||||
extract(epoch FROM clock_timestamp()) * 1000
|
||||
)::bigint AS now`,
|
||||
);
|
||||
if (clock.rows.length !== 1) throw unavailable();
|
||||
const committedAtMs = requiredInteger(clock.rows[0]!, 'now');
|
||||
const catalog = normalizeToolResultKeyCatalogRecord({
|
||||
...command.next,
|
||||
committedAtMs,
|
||||
});
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."tool_result_key_catalog_generations" (
|
||||
authority, generation, previous_generation,
|
||||
previous_catalog_digest, active_key_id, mutation_kind,
|
||||
mutation_id, catalog_digest, command_digest, committed_at_ms,
|
||||
catalog_json
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
|
||||
$11::jsonb
|
||||
)`,
|
||||
[
|
||||
AUTHORITY,
|
||||
catalog.generation,
|
||||
catalog.generation === 1 ? null : catalog.generation - 1,
|
||||
catalog.previousCatalogDigest,
|
||||
catalog.activeKeyId,
|
||||
catalog.mutationKind,
|
||||
catalog.mutationId,
|
||||
catalog.catalogDigest,
|
||||
command.commandDigest,
|
||||
catalog.committedAtMs,
|
||||
JSON.stringify(catalog),
|
||||
],
|
||||
);
|
||||
await client.query('COMMIT');
|
||||
return Object.freeze({
|
||||
status: 'created' as const,
|
||||
catalog,
|
||||
});
|
||||
} catch (error) {
|
||||
await rollback(client);
|
||||
throw mapStorageError(error);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,697 @@
|
||||
import type {
|
||||
PostgresClient,
|
||||
PostgresPool,
|
||||
PostgresQueryable,
|
||||
} from '@qinglong/runtime-core';
|
||||
import {
|
||||
InvalidToolExecutionResultRekeyError,
|
||||
ToolExecutionResultRekeyConflictError,
|
||||
ToolExecutionResultRekeyUnavailableError,
|
||||
ToolResultKeyRetirementCoverageBuilder,
|
||||
createToolResultKeyRetirementReceipt,
|
||||
normalizeToolExecutionResultRekeyCommand,
|
||||
normalizeToolExecutionResultRekeyOverlay,
|
||||
normalizeToolResultKeyRetirementReceipt,
|
||||
normalizeToolResultKeyRetirementReceiptCommand,
|
||||
type CommitToolExecutionResultRekeyResult,
|
||||
type CommitToolResultKeyRetirementReceiptResult,
|
||||
type ToolExecutionResultRekeyCommand,
|
||||
type ToolExecutionResultRekeyOverlay,
|
||||
type ToolExecutionResultRekeyReader,
|
||||
type ToolExecutionResultRekeyRepository,
|
||||
type ToolResultKeyRetirementReceipt,
|
||||
type ToolResultKeyRetirementReceiptCommand,
|
||||
type ToolResultKeyRetirementReceiptRepository,
|
||||
} from '@qinglong/runtime-core/tool-result-rekey';
|
||||
import {
|
||||
normalizeToolResultKeyCatalogRecord,
|
||||
requireActiveToolResultKey,
|
||||
toolResultKeyCatalogFence,
|
||||
type ToolResultKeyCatalogRecord,
|
||||
} from '@qinglong/runtime-core/tool-result-key-catalog';
|
||||
|
||||
import {
|
||||
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
|
||||
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
|
||||
configurePostgresDefinitionTransaction,
|
||||
postgresRequiredInteger,
|
||||
postgresRequiredJsonObject,
|
||||
postgresRequiredString,
|
||||
postgresSqlState,
|
||||
rollbackPostgresDefinitionTransaction,
|
||||
} from '../repository/definitionRepositorySupport';
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
type Queryable = Pick<PostgresQueryable, 'query'>;
|
||||
|
||||
const AUTHORITY = 'trusted-tool-results';
|
||||
const CATALOG_TRANSACTION_LOCK = 'SELECT pg_advisory_xact_lock(190397473, 3)';
|
||||
const IDENTITY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
const COVERAGE_PAGE_SIZE = 64;
|
||||
|
||||
function unavailable(
|
||||
cause?: unknown,
|
||||
): ToolExecutionResultRekeyUnavailableError {
|
||||
return new ToolExecutionResultRekeyUnavailableError({
|
||||
cause: cause instanceof Error ? cause : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function requiredText(row: Row, key: string): string {
|
||||
return postgresRequiredString(row[key], unavailable);
|
||||
}
|
||||
|
||||
function nullableText(row: Row, key: string): string | null {
|
||||
const value = row[key];
|
||||
if (value !== null && typeof value !== 'string') throw unavailable();
|
||||
return value;
|
||||
}
|
||||
|
||||
function requiredInteger(row: Row, key: string): number {
|
||||
const value = postgresRequiredInteger(row[key], unavailable);
|
||||
if (value < 0) throw unavailable();
|
||||
return value;
|
||||
}
|
||||
|
||||
function postgresConstraint(error: unknown): boolean {
|
||||
const state = postgresSqlState(error);
|
||||
return state === '23503' || state === '23505' || state === '23514';
|
||||
}
|
||||
|
||||
function mapStorageError(error: unknown): Error {
|
||||
if (
|
||||
error instanceof InvalidToolExecutionResultRekeyError ||
|
||||
error instanceof ToolExecutionResultRekeyConflictError ||
|
||||
error instanceof ToolExecutionResultRekeyUnavailableError
|
||||
) {
|
||||
return error;
|
||||
}
|
||||
return postgresConstraint(error)
|
||||
? new ToolExecutionResultRekeyConflictError()
|
||||
: unavailable(error);
|
||||
}
|
||||
|
||||
function overlayFromRow(
|
||||
row: Row,
|
||||
): Readonly<ToolExecutionResultRekeyOverlay> {
|
||||
let overlay: Readonly<ToolExecutionResultRekeyOverlay>;
|
||||
try {
|
||||
overlay = normalizeToolExecutionResultRekeyOverlay(
|
||||
postgresRequiredJsonObject(
|
||||
row.overlayJson,
|
||||
unavailable,
|
||||
) as unknown as ToolExecutionResultRekeyOverlay,
|
||||
);
|
||||
} catch {
|
||||
throw unavailable();
|
||||
}
|
||||
if (
|
||||
Buffer.byteLength(JSON.stringify(overlay), 'utf8') > 384 * 1024 ||
|
||||
overlay.overlayId !== requiredText(row, 'overlayId') ||
|
||||
overlay.sourceArtifact.artifactId !== requiredText(row, 'artifactId') ||
|
||||
overlay.sourceBindingDigest !== requiredText(row, 'sourceBindingDigest') ||
|
||||
overlay.revision !== requiredInteger(row, 'revision') ||
|
||||
overlay.previousOverlayDigest !== nullableText(row, 'previousOverlayDigest') ||
|
||||
overlay.fromKeyId !== requiredText(row, 'fromKeyId') ||
|
||||
overlay.targetCatalogFence.generation !==
|
||||
requiredInteger(row, 'targetCatalogGeneration') ||
|
||||
overlay.targetCatalogFence.catalogDigest !==
|
||||
requiredText(row, 'targetCatalogDigest') ||
|
||||
overlay.targetCatalogFence.keyId !== requiredText(row, 'targetKeyId') ||
|
||||
overlay.targetCatalogFence.materialProof !==
|
||||
requiredText(row, 'targetMaterialProof') ||
|
||||
overlay.overlayDigest !== requiredText(row, 'overlayDigest') ||
|
||||
overlay.rekeyedAtMs !== requiredInteger(row, 'rekeyedAtMs')
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
return overlay;
|
||||
}
|
||||
|
||||
function receiptFromRow(
|
||||
row: Row,
|
||||
): Readonly<ToolResultKeyRetirementReceipt> {
|
||||
let receipt: Readonly<ToolResultKeyRetirementReceipt>;
|
||||
try {
|
||||
receipt = normalizeToolResultKeyRetirementReceipt(
|
||||
postgresRequiredJsonObject(
|
||||
row.receiptJson,
|
||||
unavailable,
|
||||
) as unknown as ToolResultKeyRetirementReceipt,
|
||||
);
|
||||
} catch {
|
||||
throw unavailable();
|
||||
}
|
||||
if (
|
||||
Buffer.byteLength(JSON.stringify(receipt), 'utf8') > 64 * 1024 ||
|
||||
receipt.receiptDigest !== requiredText(row, 'receiptDigest') ||
|
||||
receipt.catalogGeneration !== requiredInteger(row, 'catalogGeneration') ||
|
||||
receipt.catalogDigest !== requiredText(row, 'catalogDigest') ||
|
||||
receipt.keyId !== requiredText(row, 'keyId') ||
|
||||
receipt.materialProof !== requiredText(row, 'materialProof') ||
|
||||
receipt.mutationId !== requiredText(row, 'mutationId') ||
|
||||
receipt.bindingCount !== requiredInteger(row, 'bindingCount') ||
|
||||
receipt.overlayHeadCount !== requiredInteger(row, 'overlayHeadCount') ||
|
||||
receipt.uncoveredBindingCount !==
|
||||
requiredInteger(row, 'uncoveredBindingCount') ||
|
||||
receipt.uncoveredOverlayHeadCount !==
|
||||
requiredInteger(row, 'uncoveredOverlayHeadCount') ||
|
||||
receipt.coverageDigest !== requiredText(row, 'coverageDigest') ||
|
||||
receipt.createdAtMs !== requiredInteger(row, 'createdAtMs')
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
return receipt;
|
||||
}
|
||||
|
||||
async function currentCatalog(
|
||||
queryable: Queryable,
|
||||
): Promise<Readonly<ToolResultKeyCatalogRecord>> {
|
||||
const result = await queryable.query<Row>(
|
||||
`SELECT catalog_json AS "catalogJson"
|
||||
FROM "ql3"."tool_result_key_catalog_generations"
|
||||
WHERE authority = $1
|
||||
ORDER BY generation DESC
|
||||
LIMIT 1`,
|
||||
[AUTHORITY],
|
||||
);
|
||||
if (result.rows.length !== 1) throw unavailable();
|
||||
try {
|
||||
return normalizeToolResultKeyCatalogRecord(
|
||||
postgresRequiredJsonObject(
|
||||
result.rows[0]!.catalogJson,
|
||||
unavailable,
|
||||
) as unknown as ToolResultKeyCatalogRecord,
|
||||
);
|
||||
} catch {
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
async function overlayRows(
|
||||
queryable: Queryable,
|
||||
suffix: string,
|
||||
values: readonly unknown[],
|
||||
): Promise<readonly Row[]> {
|
||||
const result = await queryable.query<Row>(
|
||||
`SELECT
|
||||
overlay.overlay_id AS "overlayId",
|
||||
overlay.artifact_id AS "artifactId",
|
||||
overlay.source_binding_digest AS "sourceBindingDigest",
|
||||
overlay.revision AS "revision",
|
||||
overlay.previous_overlay_digest AS "previousOverlayDigest",
|
||||
overlay.from_key_id AS "fromKeyId",
|
||||
overlay.target_catalog_generation AS "targetCatalogGeneration",
|
||||
overlay.target_catalog_digest AS "targetCatalogDigest",
|
||||
overlay.target_key_id AS "targetKeyId",
|
||||
overlay.target_material_proof AS "targetMaterialProof",
|
||||
overlay.mutation_id AS "mutationId",
|
||||
overlay.command_digest AS "commandDigest",
|
||||
overlay.overlay_digest AS "overlayDigest",
|
||||
overlay.rekeyed_at_ms AS "rekeyedAtMs",
|
||||
overlay.overlay_json AS "overlayJson"
|
||||
FROM "ql3"."tool_execution_result_rekey_overlays" AS overlay
|
||||
${suffix}
|
||||
ORDER BY overlay.revision DESC
|
||||
LIMIT 2`,
|
||||
values,
|
||||
);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
async function receiptRows(
|
||||
queryable: Queryable,
|
||||
suffix: string,
|
||||
values: readonly unknown[],
|
||||
): Promise<readonly Row[]> {
|
||||
const result = await queryable.query<Row>(
|
||||
`SELECT
|
||||
receipt_digest AS "receiptDigest",
|
||||
catalog_generation AS "catalogGeneration",
|
||||
catalog_digest AS "catalogDigest",
|
||||
key_id AS "keyId",
|
||||
material_proof AS "materialProof",
|
||||
mutation_id AS "mutationId",
|
||||
command_digest AS "commandDigest",
|
||||
binding_count AS "bindingCount",
|
||||
overlay_head_count AS "overlayHeadCount",
|
||||
uncovered_binding_count AS "uncoveredBindingCount",
|
||||
uncovered_overlay_head_count AS "uncoveredOverlayHeadCount",
|
||||
coverage_digest AS "coverageDigest",
|
||||
created_at_ms AS "createdAtMs",
|
||||
receipt_json AS "receiptJson"
|
||||
FROM "ql3"."tool_result_key_retirement_receipts"
|
||||
${suffix}
|
||||
ORDER BY created_at_ms DESC
|
||||
LIMIT 2`,
|
||||
values,
|
||||
);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
export class PostgresToolResultRekeyReader
|
||||
implements ToolExecutionResultRekeyReader
|
||||
{
|
||||
constructor(protected readonly pool: PostgresPool) {
|
||||
if (
|
||||
!pool ||
|
||||
typeof pool.query !== 'function' ||
|
||||
typeof pool.connect !== 'function'
|
||||
) {
|
||||
throw new TypeError('PostgreSQL Tool result rekey pool is invalid');
|
||||
}
|
||||
}
|
||||
|
||||
async findHeadByArtifactId(
|
||||
artifactId: string,
|
||||
): Promise<Readonly<ToolExecutionResultRekeyOverlay> | null> {
|
||||
if (!IDENTITY_PATTERN.test(artifactId)) {
|
||||
throw new InvalidToolExecutionResultRekeyError(
|
||||
'source Artifact id is invalid',
|
||||
);
|
||||
}
|
||||
try {
|
||||
const rows = await overlayRows(
|
||||
this.pool,
|
||||
`JOIN "ql3"."tool_execution_result_rekey_heads" AS head
|
||||
ON head.artifact_id = overlay.artifact_id
|
||||
AND head.revision = overlay.revision
|
||||
AND head.overlay_digest = overlay.overlay_digest
|
||||
WHERE overlay.artifact_id = $1`,
|
||||
[artifactId],
|
||||
);
|
||||
if (rows.length > 1) throw unavailable();
|
||||
return rows[0] ? overlayFromRow(rows[0]) : null;
|
||||
} catch (error) {
|
||||
throw mapStorageError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class PostgresToolResultRekeyRepository
|
||||
extends PostgresToolResultRekeyReader
|
||||
implements
|
||||
ToolExecutionResultRekeyRepository,
|
||||
ToolResultKeyRetirementReceiptRepository
|
||||
{
|
||||
private async transaction<T>(
|
||||
work: (client: PostgresClient) => Promise<T>,
|
||||
): Promise<T> {
|
||||
for (
|
||||
let attempt = 0;
|
||||
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
|
||||
attempt += 1
|
||||
) {
|
||||
let client: PostgresClient;
|
||||
try {
|
||||
client = await this.pool.connect();
|
||||
} catch (error) {
|
||||
throw mapStorageError(error);
|
||||
}
|
||||
let began = false;
|
||||
try {
|
||||
await configurePostgresDefinitionTransaction(client);
|
||||
began = true;
|
||||
await client.query(CATALOG_TRANSACTION_LOCK);
|
||||
const result = await work(client);
|
||||
await client.query('COMMIT');
|
||||
began = false;
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (began) await rollbackPostgresDefinitionTransaction(client);
|
||||
const state = postgresSqlState(error);
|
||||
if (
|
||||
state &&
|
||||
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
|
||||
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
throw mapStorageError(error);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
throw unavailable();
|
||||
}
|
||||
|
||||
append(
|
||||
commandValue: Readonly<ToolExecutionResultRekeyCommand>,
|
||||
): Promise<Readonly<CommitToolExecutionResultRekeyResult>> {
|
||||
const command = normalizeToolExecutionResultRekeyCommand(commandValue);
|
||||
return this.transaction(async (client) => {
|
||||
const replayRows = await overlayRows(
|
||||
client,
|
||||
`WHERE overlay.mutation_id = $1
|
||||
OR overlay.overlay_id = $2
|
||||
OR overlay.overlay_digest = $3`,
|
||||
[
|
||||
command.mutationId,
|
||||
command.overlay.overlayId,
|
||||
command.overlay.overlayDigest,
|
||||
],
|
||||
);
|
||||
if (replayRows.length > 1) {
|
||||
throw new ToolExecutionResultRekeyConflictError();
|
||||
}
|
||||
if (replayRows[0]) {
|
||||
const stored = overlayFromRow(replayRows[0]);
|
||||
if (
|
||||
requiredText(replayRows[0], 'commandDigest') !==
|
||||
command.commandDigest ||
|
||||
JSON.stringify(stored) !== JSON.stringify(command.overlay)
|
||||
) {
|
||||
throw new ToolExecutionResultRekeyConflictError();
|
||||
}
|
||||
return Object.freeze({
|
||||
status: 'existing' as const,
|
||||
overlay: stored,
|
||||
});
|
||||
}
|
||||
|
||||
const source = await client.query<Row>(
|
||||
`SELECT
|
||||
binding.artifact_digest AS "bindingArtifactDigest",
|
||||
binding.key_id AS "bindingKeyId",
|
||||
binding.binding_digest AS "bindingDigest",
|
||||
completion.artifact_digest AS "artifactDigest",
|
||||
completion.output_digest AS "outputDigest",
|
||||
completion.execution_result_digest AS "executionResultDigest"
|
||||
FROM "ql3"."tool_execution_result_key_bindings" AS binding
|
||||
JOIN "ql3"."tool_execution_completions" AS completion
|
||||
ON completion.artifact_id = binding.artifact_id
|
||||
AND completion.start_id = binding.start_id
|
||||
WHERE binding.artifact_id = $1`,
|
||||
[command.overlay.sourceArtifact.artifactId],
|
||||
);
|
||||
if (source.rows.length !== 1) {
|
||||
throw new ToolExecutionResultRekeyConflictError();
|
||||
}
|
||||
const binding = source.rows[0]!;
|
||||
if (
|
||||
requiredText(binding, 'bindingArtifactDigest') !==
|
||||
command.overlay.sourceArtifact.artifactDigest ||
|
||||
requiredText(binding, 'artifactDigest') !==
|
||||
command.overlay.sourceArtifact.artifactDigest ||
|
||||
requiredText(binding, 'outputDigest') !==
|
||||
command.overlay.sourceArtifact.outputDigest ||
|
||||
requiredText(binding, 'executionResultDigest') !==
|
||||
command.overlay.sourceArtifact.executionResultDigest ||
|
||||
requiredText(binding, 'bindingDigest') !==
|
||||
command.overlay.sourceBindingDigest
|
||||
) {
|
||||
throw new ToolExecutionResultRekeyConflictError();
|
||||
}
|
||||
|
||||
const headResult = await client.query<Row>(
|
||||
`SELECT revision, overlay_digest AS "overlayDigest",
|
||||
target_key_id AS "targetKeyId"
|
||||
FROM "ql3"."tool_execution_result_rekey_heads"
|
||||
WHERE artifact_id = $1
|
||||
FOR UPDATE`,
|
||||
[command.overlay.sourceArtifact.artifactId],
|
||||
);
|
||||
if (headResult.rows.length > 1) throw unavailable();
|
||||
const head = headResult.rows[0];
|
||||
const currentRevision = head ? requiredInteger(head, 'revision') : 0;
|
||||
const currentDigest = head ? requiredText(head, 'overlayDigest') : null;
|
||||
const fromKeyId = head
|
||||
? requiredText(head, 'targetKeyId')
|
||||
: requiredText(binding, 'bindingKeyId');
|
||||
if (
|
||||
currentRevision !== command.expectedRevision ||
|
||||
currentDigest !== command.expectedOverlayDigest ||
|
||||
command.overlay.fromKeyId !== fromKeyId
|
||||
) {
|
||||
throw new ToolExecutionResultRekeyConflictError();
|
||||
}
|
||||
|
||||
const catalog = await currentCatalog(client);
|
||||
const active = requireActiveToolResultKey(catalog);
|
||||
const fence = toolResultKeyCatalogFence(catalog, active);
|
||||
if (
|
||||
JSON.stringify(fence) !==
|
||||
JSON.stringify(command.overlay.targetCatalogFence)
|
||||
) {
|
||||
throw new ToolExecutionResultRekeyConflictError();
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."tool_execution_result_rekey_overlays" (
|
||||
overlay_id, artifact_id, source_binding_digest, revision,
|
||||
previous_overlay_digest, from_key_id,
|
||||
target_catalog_authority, target_catalog_generation,
|
||||
target_catalog_digest, target_key_id, target_material_proof,
|
||||
mutation_id, command_digest, overlay_digest, rekeyed_at_ms,
|
||||
overlay_json
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
|
||||
$14, $15, $16::jsonb
|
||||
)`,
|
||||
[
|
||||
command.overlay.overlayId,
|
||||
command.overlay.sourceArtifact.artifactId,
|
||||
command.overlay.sourceBindingDigest,
|
||||
command.overlay.revision,
|
||||
command.overlay.previousOverlayDigest,
|
||||
command.overlay.fromKeyId,
|
||||
AUTHORITY,
|
||||
fence.generation,
|
||||
fence.catalogDigest,
|
||||
fence.keyId,
|
||||
fence.materialProof,
|
||||
command.mutationId,
|
||||
command.commandDigest,
|
||||
command.overlay.overlayDigest,
|
||||
command.overlay.rekeyedAtMs,
|
||||
JSON.stringify(command.overlay),
|
||||
],
|
||||
);
|
||||
if (head) {
|
||||
const updated = await client.query(
|
||||
`UPDATE "ql3"."tool_execution_result_rekey_heads"
|
||||
SET revision = $1, overlay_id = $2, overlay_digest = $3,
|
||||
target_catalog_generation = $4,
|
||||
target_catalog_digest = $5, target_key_id = $6,
|
||||
updated_at_ms = $7
|
||||
WHERE artifact_id = $8 AND revision = $9
|
||||
AND overlay_digest = $10`,
|
||||
[
|
||||
command.overlay.revision,
|
||||
command.overlay.overlayId,
|
||||
command.overlay.overlayDigest,
|
||||
fence.generation,
|
||||
fence.catalogDigest,
|
||||
fence.keyId,
|
||||
command.overlay.rekeyedAtMs,
|
||||
command.overlay.sourceArtifact.artifactId,
|
||||
command.expectedRevision,
|
||||
command.expectedOverlayDigest,
|
||||
],
|
||||
);
|
||||
if (updated.rowCount !== 1) {
|
||||
throw new ToolExecutionResultRekeyConflictError();
|
||||
}
|
||||
} else {
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."tool_execution_result_rekey_heads" (
|
||||
artifact_id, revision, overlay_id, overlay_digest,
|
||||
target_catalog_generation, target_catalog_digest,
|
||||
target_key_id, updated_at_ms
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
||||
[
|
||||
command.overlay.sourceArtifact.artifactId,
|
||||
command.overlay.revision,
|
||||
command.overlay.overlayId,
|
||||
command.overlay.overlayDigest,
|
||||
fence.generation,
|
||||
fence.catalogDigest,
|
||||
fence.keyId,
|
||||
command.overlay.rekeyedAtMs,
|
||||
],
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
status: 'created' as const,
|
||||
overlay: command.overlay,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async findByDigest(
|
||||
receiptDigest: string,
|
||||
): Promise<Readonly<ToolResultKeyRetirementReceipt> | null> {
|
||||
if (!/^[0-9a-f]{64}$/.test(receiptDigest)) {
|
||||
throw new InvalidToolExecutionResultRekeyError(
|
||||
'retirement receipt digest is invalid',
|
||||
);
|
||||
}
|
||||
try {
|
||||
const rows = await receiptRows(
|
||||
this.pool,
|
||||
'WHERE receipt_digest = $1',
|
||||
[receiptDigest],
|
||||
);
|
||||
if (rows.length > 1) throw unavailable();
|
||||
return rows[0] ? receiptFromRow(rows[0]) : null;
|
||||
} catch (error) {
|
||||
throw mapStorageError(error);
|
||||
}
|
||||
}
|
||||
|
||||
create(
|
||||
commandValue: Readonly<ToolResultKeyRetirementReceiptCommand>,
|
||||
): Promise<Readonly<CommitToolResultKeyRetirementReceiptResult>> {
|
||||
const command =
|
||||
normalizeToolResultKeyRetirementReceiptCommand(commandValue);
|
||||
return this.transaction(async (client) => {
|
||||
const replayRows = await receiptRows(
|
||||
client,
|
||||
'WHERE mutation_id = $1',
|
||||
[command.mutationId],
|
||||
);
|
||||
if (replayRows.length > 1) {
|
||||
throw new ToolExecutionResultRekeyConflictError();
|
||||
}
|
||||
if (replayRows[0]) {
|
||||
const stored = receiptFromRow(replayRows[0]);
|
||||
if (
|
||||
requiredText(replayRows[0], 'commandDigest') !==
|
||||
command.commandDigest ||
|
||||
stored.catalogGeneration !== command.expectedCatalogGeneration ||
|
||||
stored.catalogDigest !== command.expectedCatalogDigest ||
|
||||
stored.keyId !== command.keyId
|
||||
) {
|
||||
throw new ToolExecutionResultRekeyConflictError();
|
||||
}
|
||||
return Object.freeze({
|
||||
status: 'existing' as const,
|
||||
receipt: stored,
|
||||
});
|
||||
}
|
||||
|
||||
const catalog = await currentCatalog(client);
|
||||
if (
|
||||
catalog.generation !== command.expectedCatalogGeneration ||
|
||||
catalog.catalogDigest !== command.expectedCatalogDigest
|
||||
) {
|
||||
throw new ToolExecutionResultRekeyConflictError();
|
||||
}
|
||||
const retiring = catalog.keys.find(
|
||||
(entry) => entry.keyId === command.keyId,
|
||||
);
|
||||
if (!retiring || retiring.state !== 'decrypt_only') {
|
||||
throw new ToolExecutionResultRekeyConflictError();
|
||||
}
|
||||
const decryptableKeyIds = catalog.keys
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.keyId !== retiring.keyId &&
|
||||
(entry.state === 'active' || entry.state === 'decrypt_only'),
|
||||
)
|
||||
.map((entry) => entry.keyId)
|
||||
.sort();
|
||||
const coverage = new ToolResultKeyRetirementCoverageBuilder({
|
||||
catalogGeneration: catalog.generation,
|
||||
catalogDigest: catalog.catalogDigest,
|
||||
keyId: retiring.keyId,
|
||||
decryptableKeyIds,
|
||||
});
|
||||
let cursor = '';
|
||||
for (;;) {
|
||||
const page = await client.query<Row>(
|
||||
`SELECT
|
||||
binding.artifact_id AS "artifactId",
|
||||
binding.binding_digest AS "bindingDigest",
|
||||
binding.key_id AS "bindingKeyId",
|
||||
head.overlay_digest AS "headOverlayDigest",
|
||||
head.target_key_id AS "headTargetKeyId",
|
||||
head.target_catalog_generation AS "headTargetCatalogGeneration",
|
||||
head.target_catalog_digest AS "headTargetCatalogDigest"
|
||||
FROM "ql3"."tool_execution_result_key_bindings" AS binding
|
||||
LEFT JOIN "ql3"."tool_execution_result_rekey_heads" AS head
|
||||
ON head.artifact_id = binding.artifact_id
|
||||
WHERE (binding.key_id = $1 OR head.target_key_id = $1)
|
||||
AND binding.artifact_id > $2
|
||||
ORDER BY binding.artifact_id
|
||||
LIMIT $3`,
|
||||
[retiring.keyId, cursor, COVERAGE_PAGE_SIZE],
|
||||
);
|
||||
for (const row of page.rows) {
|
||||
coverage.add({
|
||||
artifactId: requiredText(row, 'artifactId'),
|
||||
bindingDigest: requiredText(row, 'bindingDigest'),
|
||||
bindingKeyId: requiredText(row, 'bindingKeyId'),
|
||||
headOverlayDigest: nullableText(row, 'headOverlayDigest'),
|
||||
headTargetKeyId: nullableText(row, 'headTargetKeyId'),
|
||||
headTargetCatalogGeneration:
|
||||
row.headTargetCatalogGeneration === null
|
||||
? null
|
||||
: requiredInteger(row, 'headTargetCatalogGeneration'),
|
||||
headTargetCatalogDigest: nullableText(
|
||||
row,
|
||||
'headTargetCatalogDigest',
|
||||
),
|
||||
});
|
||||
}
|
||||
if (page.rows.length < COVERAGE_PAGE_SIZE) break;
|
||||
cursor = requiredText(page.rows[page.rows.length - 1]!, 'artifactId');
|
||||
}
|
||||
const result = coverage.finish();
|
||||
if (
|
||||
result.uncoveredBindingCount !== 0 ||
|
||||
result.uncoveredOverlayHeadCount !== 0
|
||||
) {
|
||||
throw new ToolExecutionResultRekeyConflictError();
|
||||
}
|
||||
const clock = await client.query<Row>(
|
||||
`SELECT floor(
|
||||
extract(epoch FROM clock_timestamp()) * 1000
|
||||
)::bigint AS now`,
|
||||
);
|
||||
if (clock.rows.length !== 1) throw unavailable();
|
||||
const receipt = createToolResultKeyRetirementReceipt({
|
||||
catalogGeneration: catalog.generation,
|
||||
catalogDigest: catalog.catalogDigest,
|
||||
keyId: retiring.keyId,
|
||||
materialProof: retiring.materialProof,
|
||||
mutationId: command.mutationId,
|
||||
bindingCount: result.bindingCount,
|
||||
overlayHeadCount: result.overlayHeadCount,
|
||||
coverageDigest: result.coverageDigest,
|
||||
createdAtMs: requiredInteger(clock.rows[0]!, 'now'),
|
||||
});
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."tool_result_key_retirement_receipts" (
|
||||
receipt_digest, catalog_authority, catalog_generation,
|
||||
catalog_digest, key_id, material_proof, mutation_id,
|
||||
command_digest, binding_count, overlay_head_count,
|
||||
uncovered_binding_count, uncovered_overlay_head_count,
|
||||
coverage_digest, created_at_ms, receipt_json
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 0, 0, $11, $12,
|
||||
$13::jsonb
|
||||
)`,
|
||||
[
|
||||
receipt.receiptDigest,
|
||||
AUTHORITY,
|
||||
receipt.catalogGeneration,
|
||||
receipt.catalogDigest,
|
||||
receipt.keyId,
|
||||
receipt.materialProof,
|
||||
receipt.mutationId,
|
||||
command.commandDigest,
|
||||
receipt.bindingCount,
|
||||
receipt.overlayHeadCount,
|
||||
receipt.coverageDigest,
|
||||
receipt.createdAtMs,
|
||||
JSON.stringify(receipt),
|
||||
],
|
||||
);
|
||||
return Object.freeze({
|
||||
status: 'created' as const,
|
||||
receipt,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user