mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-23 03:18:09 +08:00
feat(ql3): execute copilot diagnosis tools
This commit is contained in:
@@ -175,7 +175,7 @@ export function createCopilotFailureDiagnosisAdmissionBundle(
|
||||
runId: plan.runId,
|
||||
stepKey: 'collect-log',
|
||||
kind: 'tool',
|
||||
definitionRef: `trusted-tool-plan:${plan.tool.planDigest}`,
|
||||
definitionRef: 'tool:qinglong.run.log.excerpt@1.0.0',
|
||||
definitionDigest: plan.tool.definitionDigest,
|
||||
required: true,
|
||||
initialStatus: 'ready',
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './tool-execution/postgresUnlockRepository';
|
||||
@@ -0,0 +1,131 @@
|
||||
import type { RunRecord } from '@qinglong/runtime-core';
|
||||
import type { SecurityPrincipal } from '@qinglong/runtime-core/security';
|
||||
import type { StepRunMutation } from '@qinglong/runtime-core/step-run';
|
||||
import type {
|
||||
ToolExecutionCompletionRecord,
|
||||
ToolExecutionResultArtifactReference,
|
||||
} from '@qinglong/runtime-core/tool-execution-completion';
|
||||
import type { ToolPolicyAuthorizer } from '@qinglong/runtime-core/tool-registry';
|
||||
|
||||
import type {
|
||||
CopilotFailureDiagnosisAdmissionRepository,
|
||||
CopilotFailureDiagnosisExecutionPlan,
|
||||
} from '../admission/contracts';
|
||||
|
||||
export const COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_RECEIPT_SCHEMA =
|
||||
'qinglong/copilot-failure-diagnosis-tool-unlock-receipt@v1' as const;
|
||||
export const COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_COMMAND_SCHEMA =
|
||||
'qinglong/copilot-failure-diagnosis-tool-unlock-command@v1' as const;
|
||||
export const MAX_COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_RECEIPT_BYTES =
|
||||
16 * 1024;
|
||||
export const MAX_COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_COMMAND_BYTES =
|
||||
48 * 1024;
|
||||
|
||||
export interface CopilotFailureDiagnosisToolUnlockReceipt {
|
||||
readonly schema: typeof COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_RECEIPT_SCHEMA;
|
||||
readonly requestId: string;
|
||||
readonly planDigest: string;
|
||||
readonly runId: string;
|
||||
readonly startId: string;
|
||||
readonly barrierDigest: string;
|
||||
readonly toolStepRunId: string;
|
||||
readonly toolCompletionDigest: string;
|
||||
readonly resultArtifact: Readonly<ToolExecutionResultArtifactReference>;
|
||||
readonly modelStepRunId: string;
|
||||
readonly modelStepRunVersion: number;
|
||||
readonly modelStepRunDigest: string;
|
||||
readonly modelMutationId: string;
|
||||
readonly modelMutationDigest: string;
|
||||
readonly modelEventId: string;
|
||||
readonly finalRunVersion: number;
|
||||
readonly finalRunEventSequence: number;
|
||||
readonly unlockedAtMs: number;
|
||||
readonly receiptDigest: string;
|
||||
}
|
||||
|
||||
export interface CopilotFailureDiagnosisToolUnlockCommand {
|
||||
readonly schema: typeof COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_COMMAND_SCHEMA;
|
||||
readonly plan: Readonly<CopilotFailureDiagnosisExecutionPlan>;
|
||||
readonly completion: Readonly<ToolExecutionCompletionRecord>;
|
||||
readonly modelStepRunMutation: Readonly<StepRunMutation>;
|
||||
readonly receipt: Readonly<CopilotFailureDiagnosisToolUnlockReceipt>;
|
||||
readonly commandDigest: string;
|
||||
}
|
||||
|
||||
export interface CopilotFailureDiagnosisToolUnlockRepository {
|
||||
findByRequestId(
|
||||
requestId: string,
|
||||
): Promise<Readonly<CopilotFailureDiagnosisToolUnlockReceipt> | null>;
|
||||
commit(command: CopilotFailureDiagnosisToolUnlockCommand): Promise<
|
||||
Readonly<{
|
||||
status: 'created' | 'existing';
|
||||
receipt: Readonly<CopilotFailureDiagnosisToolUnlockReceipt>;
|
||||
}>
|
||||
>;
|
||||
}
|
||||
|
||||
export interface ExecuteCopilotFailureDiagnosisToolInput {
|
||||
readonly requestId: string;
|
||||
readonly principal: Readonly<SecurityPrincipal>;
|
||||
readonly authorizer: ToolPolicyAuthorizer;
|
||||
}
|
||||
|
||||
export type CopilotFailureDiagnosisToolExecutionResult =
|
||||
| Readonly<{
|
||||
outcome: 'succeeded';
|
||||
completionStatus: 'created' | 'existing';
|
||||
unlockStatus: 'created' | 'existing';
|
||||
completion: Readonly<ToolExecutionCompletionRecord>;
|
||||
unlock: Readonly<CopilotFailureDiagnosisToolUnlockReceipt>;
|
||||
}>
|
||||
| Readonly<{
|
||||
outcome: 'failed' | 'timed_out';
|
||||
completionStatus: 'created' | 'existing';
|
||||
unlockStatus: null;
|
||||
}>;
|
||||
|
||||
export class InvalidCopilotFailureDiagnosisToolExecutionError extends TypeError {
|
||||
readonly code = 'COPILOT_FAILURE_DIAGNOSIS_TOOL_EXECUTION_INVALID';
|
||||
|
||||
constructor(message: string) {
|
||||
super(`Copilot failure diagnosis Tool execution is invalid: ${message}`);
|
||||
this.name = 'InvalidCopilotFailureDiagnosisToolExecutionError';
|
||||
}
|
||||
}
|
||||
|
||||
export class CopilotFailureDiagnosisToolExecutionConflictError extends Error {
|
||||
readonly code = 'COPILOT_FAILURE_DIAGNOSIS_TOOL_EXECUTION_CONFLICT';
|
||||
|
||||
constructor(message = 'durable Tool execution facts changed') {
|
||||
super(`Copilot failure diagnosis Tool execution conflicts: ${message}`);
|
||||
this.name = 'CopilotFailureDiagnosisToolExecutionConflictError';
|
||||
}
|
||||
}
|
||||
|
||||
export class CopilotFailureDiagnosisToolExecutionUnavailableError extends Error {
|
||||
readonly code = 'COPILOT_FAILURE_DIAGNOSIS_TOOL_EXECUTION_UNAVAILABLE';
|
||||
|
||||
constructor(options?: ErrorOptions) {
|
||||
super('Copilot failure diagnosis Tool execution is unavailable', options);
|
||||
this.name = 'CopilotFailureDiagnosisToolExecutionUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
export class CopilotFailureDiagnosisToolExecutionDeadlineExceededError extends Error {
|
||||
readonly code = 'COPILOT_FAILURE_DIAGNOSIS_TOOL_EXECUTION_DEADLINE_EXCEEDED';
|
||||
|
||||
constructor() {
|
||||
super('Copilot failure diagnosis Tool execution deadline was exceeded');
|
||||
this.name = 'CopilotFailureDiagnosisToolExecutionDeadlineExceededError';
|
||||
}
|
||||
}
|
||||
|
||||
export type CopilotFailureDiagnosisToolExecutionAdmissionReader = Pick<
|
||||
CopilotFailureDiagnosisAdmissionRepository,
|
||||
'findByRequestId' | 'findPlanByRequestId'
|
||||
>;
|
||||
|
||||
export type CopilotFailureDiagnosisRunAuthority = Pick<
|
||||
RunRecord,
|
||||
'id' | 'projectId' | 'status' | 'version' | 'eventSequence'
|
||||
>;
|
||||
@@ -0,0 +1,599 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
import type { RunRepositoryReader } from '@qinglong/runtime-core/run-repository';
|
||||
import {
|
||||
transitionStepRunMutation,
|
||||
type StepRunRepository,
|
||||
} from '@qinglong/runtime-core/step-run';
|
||||
import {
|
||||
createToolExecutionEvidenceBundle,
|
||||
toolExecutionAdmissionEvidence,
|
||||
TOOL_EXECUTION_START_AUDIT_OPERATION,
|
||||
} from '@qinglong/runtime-core/tool-execution-evidence';
|
||||
import {
|
||||
createToolExecutionStartCommand,
|
||||
normalizeToolExecutionStartBarrierRecord,
|
||||
type ToolExecutionStartBarrierRecord,
|
||||
type ToolExecutionStartBarrierRepository,
|
||||
} from '@qinglong/runtime-core/tool-execution-start-barrier';
|
||||
import type { ToolExecutionCompletionRepository } from '@qinglong/runtime-core/tool-execution-completion';
|
||||
import type { ToolExecutionFailureCompletionRepository } from '@qinglong/runtime-core/tool-execution-failure-completion';
|
||||
import type { ToolExecutionResultRekeyReader } from '@qinglong/runtime-core/tool-result-rekey';
|
||||
import type { ToolResultKeyCatalogReader } from '@qinglong/runtime-core/tool-result-key-catalog';
|
||||
import type { ProjectToolDefinitionSnapshotRepository } from '@qinglong/runtime-core/project-tool-definition-snapshot';
|
||||
import {
|
||||
toolInvocationInputArtifactReference,
|
||||
toolInvocationPreviewArtifactReference,
|
||||
type ToolInvocationArtifactKeyProvider,
|
||||
type ToolInvocationArtifactRepository,
|
||||
} from '@qinglong/runtime-core/tool-invocation-artifact';
|
||||
import {
|
||||
admitTrustedToolExecution,
|
||||
trustedToolContractIdentityDigest,
|
||||
} from '@qinglong/runtime-core/trusted-tool-invocation';
|
||||
import { TrustedToolExecutionAdapterRegistry } from '@qinglong/runtime-core/trusted-tool-execution';
|
||||
import { executeAndCompleteTrustedTool } from '@qinglong/runtime-core/trusted-tool-completion';
|
||||
import { BuiltInRunLogExcerptToolAdapter } from '@qinglong/runtime-core/builtin-run-log-excerpt-tool';
|
||||
import type { RunAttemptLogReadPort } from '@qinglong/runtime-core/builtin-run-log-excerpt-projection';
|
||||
|
||||
import {
|
||||
CopilotFailureDiagnosisToolExecutionConflictError,
|
||||
CopilotFailureDiagnosisToolExecutionDeadlineExceededError,
|
||||
CopilotFailureDiagnosisToolExecutionUnavailableError,
|
||||
InvalidCopilotFailureDiagnosisToolExecutionError,
|
||||
type CopilotFailureDiagnosisToolExecutionAdmissionReader,
|
||||
type CopilotFailureDiagnosisToolExecutionResult,
|
||||
type CopilotFailureDiagnosisToolUnlockRepository,
|
||||
type ExecuteCopilotFailureDiagnosisToolInput,
|
||||
} from './contracts';
|
||||
import { restoreCopilotFailureDiagnosisTrustedToolAuthority } from './planAuthority';
|
||||
import { createCopilotFailureDiagnosisToolUnlockCommand } from './unlockProtocol';
|
||||
|
||||
export interface CopilotFailureDiagnosisToolExecutionDependencies {
|
||||
readonly admissions: CopilotFailureDiagnosisToolExecutionAdmissionReader;
|
||||
readonly snapshots: Pick<
|
||||
ProjectToolDefinitionSnapshotRepository,
|
||||
'findCurrent'
|
||||
>;
|
||||
readonly artifacts: ToolInvocationArtifactRepository;
|
||||
readonly invocationKeys: Pick<ToolInvocationArtifactKeyProvider, 'resolve'>;
|
||||
readonly resultKeys: Pick<ToolInvocationArtifactKeyProvider, 'resolve'>;
|
||||
readonly stepRuns: Pick<StepRunRepository, 'findById'>;
|
||||
readonly runs: Pick<RunRepositoryReader, 'findRunById'>;
|
||||
readonly barriers: ToolExecutionStartBarrierRepository;
|
||||
readonly completions: ToolExecutionCompletionRepository;
|
||||
readonly failureCompletions: ToolExecutionFailureCompletionRepository;
|
||||
readonly resultKeyCatalog: ToolResultKeyCatalogReader;
|
||||
readonly resultRekeys: ToolExecutionResultRekeyReader;
|
||||
readonly logs: RunAttemptLogReadPort;
|
||||
readonly unlocks: CopilotFailureDiagnosisToolUnlockRepository;
|
||||
readonly now?: () => number;
|
||||
readonly nonceFactory?: () => Uint8Array;
|
||||
}
|
||||
|
||||
const IDENTITY_DOMAIN = Buffer.from(
|
||||
'qinglong/copilot-failure-diagnosis-tool-execution-identity@v1\0',
|
||||
'utf8',
|
||||
);
|
||||
|
||||
function invalid(message: string): never {
|
||||
throw new InvalidCopilotFailureDiagnosisToolExecutionError(message);
|
||||
}
|
||||
|
||||
function unavailable(cause?: unknown): never {
|
||||
throw new CopilotFailureDiagnosisToolExecutionUnavailableError({
|
||||
cause: cause instanceof Error ? cause : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function hash(value: unknown): string {
|
||||
return createHash('sha256')
|
||||
.update(IDENTITY_DOMAIN)
|
||||
.update(JSON.stringify(value))
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
function identity(prefix: string, planDigest: string): string {
|
||||
const maximumDigestLength = 35 - prefix.length;
|
||||
return `${prefix}:${hash({ prefix, planDigest }).slice(
|
||||
0,
|
||||
maximumDigestLength,
|
||||
)}`;
|
||||
}
|
||||
|
||||
function traceIdentity(planDigest: string): string {
|
||||
return hash({ prefix: 'trace', planDigest }).slice(0, 32);
|
||||
}
|
||||
|
||||
function spanIdentity(planDigest: string): string {
|
||||
return hash({ prefix: 'span', planDigest }).slice(0, 16);
|
||||
}
|
||||
|
||||
function auditEventIdentity(planDigest: string): string {
|
||||
const value = hash({ prefix: 'audit', planDigest }).slice(0, 32).split('');
|
||||
value[12] = '4';
|
||||
value[16] = '8';
|
||||
const hex = value.join('');
|
||||
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(
|
||||
12,
|
||||
16,
|
||||
)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
||||
}
|
||||
|
||||
function clock(now: (() => number) | undefined): number {
|
||||
let value: number;
|
||||
try {
|
||||
value = (now ?? Date.now)();
|
||||
} catch (cause) {
|
||||
return unavailable(cause);
|
||||
}
|
||||
if (!Number.isSafeInteger(value) || value < 0) return unavailable();
|
||||
return value;
|
||||
}
|
||||
|
||||
function sameValue(left: unknown, right: unknown): boolean {
|
||||
return JSON.stringify(left) === JSON.stringify(right);
|
||||
}
|
||||
|
||||
function assertDependencies(
|
||||
dependencies: CopilotFailureDiagnosisToolExecutionDependencies,
|
||||
): void {
|
||||
if (
|
||||
!dependencies ||
|
||||
typeof dependencies !== 'object' ||
|
||||
typeof dependencies.admissions?.findByRequestId !== 'function' ||
|
||||
typeof dependencies.admissions?.findPlanByRequestId !== 'function' ||
|
||||
typeof dependencies.snapshots?.findCurrent !== 'function' ||
|
||||
typeof dependencies.artifacts?.findInput !== 'function' ||
|
||||
typeof dependencies.artifacts?.findPreview !== 'function' ||
|
||||
typeof dependencies.invocationKeys?.resolve !== 'function' ||
|
||||
typeof dependencies.resultKeys?.resolve !== 'function' ||
|
||||
typeof dependencies.stepRuns?.findById !== 'function' ||
|
||||
typeof dependencies.runs?.findRunById !== 'function' ||
|
||||
typeof dependencies.barriers?.findByStartId !== 'function' ||
|
||||
typeof dependencies.barriers?.prepare !== 'function' ||
|
||||
typeof dependencies.completions?.findByStartId !== 'function' ||
|
||||
typeof dependencies.failureCompletions?.findByStartId !== 'function' ||
|
||||
typeof dependencies.resultKeyCatalog?.findCurrent !== 'function' ||
|
||||
typeof dependencies.resultRekeys?.findHeadByArtifactId !== 'function' ||
|
||||
typeof dependencies.logs?.read !== 'function' ||
|
||||
typeof dependencies.unlocks?.findByRequestId !== 'function' ||
|
||||
typeof dependencies.unlocks?.commit !== 'function' ||
|
||||
(dependencies.now !== undefined &&
|
||||
typeof dependencies.now !== 'function') ||
|
||||
(dependencies.nonceFactory !== undefined &&
|
||||
typeof dependencies.nonceFactory !== 'function')
|
||||
) {
|
||||
return invalid('dependencies are invalid');
|
||||
}
|
||||
}
|
||||
|
||||
async function durablePlan(
|
||||
requestId: string,
|
||||
dependencies: CopilotFailureDiagnosisToolExecutionDependencies,
|
||||
) {
|
||||
let plan;
|
||||
let receipt;
|
||||
try {
|
||||
[plan, receipt] = await Promise.all([
|
||||
dependencies.admissions.findPlanByRequestId(requestId),
|
||||
dependencies.admissions.findByRequestId(requestId),
|
||||
]);
|
||||
} catch (cause) {
|
||||
return unavailable(cause);
|
||||
}
|
||||
if (
|
||||
!plan ||
|
||||
!receipt ||
|
||||
plan.requestId !== requestId ||
|
||||
receipt.requestId !== requestId ||
|
||||
receipt.planDigest !== plan.planDigest ||
|
||||
receipt.runId !== plan.runId ||
|
||||
receipt.toolStepRunId !== plan.toolStepRunId ||
|
||||
receipt.modelStepRunId !== plan.modelStepRunId
|
||||
) {
|
||||
throw new CopilotFailureDiagnosisToolExecutionConflictError(
|
||||
'diagnosis admission evidence is incomplete',
|
||||
);
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
async function currentAuthority(
|
||||
plan: Awaited<ReturnType<typeof durablePlan>>,
|
||||
dependencies: CopilotFailureDiagnosisToolExecutionDependencies,
|
||||
) {
|
||||
let record;
|
||||
try {
|
||||
record = await dependencies.snapshots.findCurrent(plan.projectId);
|
||||
} catch (cause) {
|
||||
return unavailable(cause);
|
||||
}
|
||||
if (!record) {
|
||||
throw new CopilotFailureDiagnosisToolExecutionConflictError(
|
||||
'the current Project Tool snapshot is absent',
|
||||
);
|
||||
}
|
||||
return restoreCopilotFailureDiagnosisTrustedToolAuthority(
|
||||
plan,
|
||||
record.snapshot,
|
||||
);
|
||||
}
|
||||
|
||||
async function assertArtifacts(
|
||||
plan: Awaited<ReturnType<typeof durablePlan>>,
|
||||
dependencies: CopilotFailureDiagnosisToolExecutionDependencies,
|
||||
): Promise<void> {
|
||||
let input;
|
||||
let preview;
|
||||
try {
|
||||
[input, preview] = await Promise.all([
|
||||
dependencies.artifacts.findInput(plan.tool.invocationArtifact.artifactId),
|
||||
dependencies.artifacts.findPreview(plan.tool.previewArtifact.artifactId),
|
||||
]);
|
||||
} catch (cause) {
|
||||
return unavailable(cause);
|
||||
}
|
||||
if (
|
||||
!input ||
|
||||
!preview ||
|
||||
!sameValue(
|
||||
toolInvocationInputArtifactReference(input),
|
||||
plan.tool.invocationArtifact,
|
||||
) ||
|
||||
!sameValue(
|
||||
toolInvocationPreviewArtifactReference(preview),
|
||||
plan.tool.previewArtifact,
|
||||
) ||
|
||||
input.projectId !== plan.projectId ||
|
||||
input.actionRef !== plan.tool.actionRef ||
|
||||
preview.projectId !== plan.projectId ||
|
||||
preview.actionRef !== plan.tool.actionRef ||
|
||||
input.sealedAtMs !== plan.tool.sealedAtMs ||
|
||||
preview.sealedAtMs !== plan.tool.sealedAtMs
|
||||
) {
|
||||
throw new CopilotFailureDiagnosisToolExecutionConflictError(
|
||||
'the durable Tool invocation Artifacts changed',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function barrierMatches(
|
||||
barrierValue: ToolExecutionStartBarrierRecord,
|
||||
plan: Awaited<ReturnType<typeof durablePlan>>,
|
||||
startId: string,
|
||||
): Readonly<ToolExecutionStartBarrierRecord> {
|
||||
const barrier = normalizeToolExecutionStartBarrierRecord(barrierValue);
|
||||
if (
|
||||
barrier.startId !== startId ||
|
||||
barrier.projectId !== plan.projectId ||
|
||||
barrier.runId !== plan.runId ||
|
||||
barrier.stepRunId !== plan.toolStepRunId ||
|
||||
barrier.actionRef !== plan.tool.actionRef ||
|
||||
barrier.planDigest !== plan.tool.planDigest ||
|
||||
barrier.actionDigest !== plan.tool.actionDigest ||
|
||||
barrier.snapshotDigest !== plan.tool.snapshotDigest ||
|
||||
barrier.definitionDigest !== plan.tool.definitionDigest ||
|
||||
barrier.bindingDigest !== plan.tool.bindingDigest ||
|
||||
!sameValue(barrier.invocationArtifact, plan.tool.invocationArtifact) ||
|
||||
!sameValue(barrier.previewArtifact, plan.tool.previewArtifact) ||
|
||||
!sameValue(barrier.requestedBy, plan.requestedBySubject) ||
|
||||
barrier.profile !== 'cluster-control' ||
|
||||
!sameValue(barrier.policyFence, plan.policyFence) ||
|
||||
barrier.approvalRequestId !== null ||
|
||||
barrier.approvalDispatchId !== null ||
|
||||
barrier.approvalDispatchDigest !== null ||
|
||||
barrier.previousStepRunVersion !== 1 ||
|
||||
barrier.startedStepRunVersion !== 2
|
||||
) {
|
||||
throw new CopilotFailureDiagnosisToolExecutionConflictError(
|
||||
'the durable Tool start barrier changed',
|
||||
);
|
||||
}
|
||||
return barrier;
|
||||
}
|
||||
|
||||
async function prepareStart(
|
||||
plan: Awaited<ReturnType<typeof durablePlan>>,
|
||||
input: ExecuteCopilotFailureDiagnosisToolInput,
|
||||
authority: Awaited<ReturnType<typeof currentAuthority>>,
|
||||
dependencies: CopilotFailureDiagnosisToolExecutionDependencies,
|
||||
startId: string,
|
||||
): Promise<Readonly<ToolExecutionStartBarrierRecord>> {
|
||||
let existing;
|
||||
try {
|
||||
existing = await dependencies.barriers.findByStartId(startId);
|
||||
} catch (cause) {
|
||||
return unavailable(cause);
|
||||
}
|
||||
if (existing) return barrierMatches(existing, plan, startId);
|
||||
|
||||
await assertArtifacts(plan, dependencies);
|
||||
let stepRun;
|
||||
let run;
|
||||
try {
|
||||
[stepRun, run] = await Promise.all([
|
||||
dependencies.stepRuns.findById(plan.toolStepRunId),
|
||||
dependencies.runs.findRunById(plan.runId),
|
||||
]);
|
||||
} catch (cause) {
|
||||
return unavailable(cause);
|
||||
}
|
||||
if (
|
||||
!stepRun ||
|
||||
!run ||
|
||||
stepRun.id !== plan.toolStepRunId ||
|
||||
stepRun.runId !== plan.runId ||
|
||||
stepRun.kind !== 'tool' ||
|
||||
stepRun.status !== 'ready' ||
|
||||
stepRun.version !== 1 ||
|
||||
stepRun.definitionRef !== 'tool:qinglong.run.log.excerpt@1.0.0' ||
|
||||
stepRun.definitionDigest !== plan.tool.definitionDigest ||
|
||||
run.id !== plan.runId ||
|
||||
run.projectId !== plan.projectId ||
|
||||
run.status !== 'running' ||
|
||||
run.version !== run.eventSequence
|
||||
) {
|
||||
throw new CopilotFailureDiagnosisToolExecutionConflictError(
|
||||
'the Tool StepRun is not startable',
|
||||
);
|
||||
}
|
||||
const startedAtMs = clock(dependencies.now);
|
||||
if (
|
||||
startedAtMs < plan.plannedAtMs ||
|
||||
startedAtMs + authority.plan.timeoutSeconds * 1_000 > plan.deadlineAtMs
|
||||
) {
|
||||
throw new CopilotFailureDiagnosisToolExecutionDeadlineExceededError();
|
||||
}
|
||||
const evidence = createToolExecutionEvidenceBundle({
|
||||
traceId: traceIdentity(plan.planDigest),
|
||||
spanId: spanIdentity(plan.planDigest),
|
||||
projectId: plan.projectId,
|
||||
runId: plan.runId,
|
||||
stepRunId: plan.toolStepRunId,
|
||||
invocationPlanDigest: authority.plan.planDigest,
|
||||
bindingDigest: authority.binding.bindingDigest,
|
||||
adapterDigest: trustedToolContractIdentityDigest(authority.binding.adapter),
|
||||
redactionContractDigest: trustedToolContractIdentityDigest(
|
||||
authority.binding.redactionContract,
|
||||
),
|
||||
auditContractDigest: trustedToolContractIdentityDigest(
|
||||
authority.binding.auditContract,
|
||||
),
|
||||
audit: {
|
||||
eventId: auditEventIdentity(plan.planDigest),
|
||||
requestId: plan.requestId,
|
||||
operationId: TOOL_EXECUTION_START_AUDIT_OPERATION,
|
||||
projectId: plan.projectId,
|
||||
subject: plan.requestedBySubject,
|
||||
authenticationId: input.principal.authenticationId,
|
||||
outcome: 'allowed',
|
||||
reasons: ['copilot_failure_diagnosis_tool_start'],
|
||||
fence: plan.policyFence,
|
||||
occurredAtMs: startedAtMs,
|
||||
},
|
||||
createdAtMs: startedAtMs,
|
||||
});
|
||||
const admission = await admitTrustedToolExecution(
|
||||
authority.bindings,
|
||||
authority.plan,
|
||||
{
|
||||
principal: input.principal,
|
||||
profile: 'cluster-control',
|
||||
nowMs: startedAtMs,
|
||||
authorizer: input.authorizer,
|
||||
evidence: {
|
||||
stepRun: {
|
||||
id: stepRun.id,
|
||||
version: stepRun.version,
|
||||
digest: stepRun.stepRunDigest,
|
||||
},
|
||||
...toolExecutionAdmissionEvidence(evidence),
|
||||
},
|
||||
},
|
||||
);
|
||||
const mutationId = identity('cdstm', plan.planDigest);
|
||||
const eventId = identity('cdste', plan.planDigest);
|
||||
const mutation = transitionStepRunMutation(
|
||||
stepRun,
|
||||
{
|
||||
expectedVersion: stepRun.version,
|
||||
expectedDigest: stepRun.stepRunDigest,
|
||||
mutationId,
|
||||
to: 'running',
|
||||
atMs: startedAtMs,
|
||||
},
|
||||
{
|
||||
expectedRunVersion: run.version,
|
||||
expectedRunEventSequence: run.eventSequence,
|
||||
eventId,
|
||||
dedupeKey: eventId,
|
||||
actor: plan.requestedBySubject,
|
||||
},
|
||||
);
|
||||
const command = createToolExecutionStartCommand({
|
||||
startId,
|
||||
admission,
|
||||
evidence,
|
||||
stepRunMutation: mutation,
|
||||
});
|
||||
try {
|
||||
const prepared = await dependencies.barriers.prepare(command);
|
||||
if (!['created', 'existing'].includes(prepared.status)) {
|
||||
return unavailable();
|
||||
}
|
||||
return barrierMatches(prepared.barrier, plan, startId);
|
||||
} catch (cause) {
|
||||
let recovered;
|
||||
try {
|
||||
recovered = await dependencies.barriers.findByStartId(startId);
|
||||
} catch {
|
||||
throw cause;
|
||||
}
|
||||
if (recovered) return barrierMatches(recovered, plan, startId);
|
||||
throw cause;
|
||||
}
|
||||
}
|
||||
|
||||
function completionIdentities(planDigest: string) {
|
||||
return Object.freeze({
|
||||
success: Object.freeze({
|
||||
create() {
|
||||
return Object.freeze({
|
||||
artifactId: identity('cdra', planDigest),
|
||||
mutationId: identity('cdscm', planDigest),
|
||||
eventId: identity('cdsce', planDigest),
|
||||
});
|
||||
},
|
||||
}),
|
||||
failure: Object.freeze({
|
||||
create() {
|
||||
return Object.freeze({
|
||||
mutationId: identity('cdfcm', planDigest),
|
||||
eventId: identity('cdfce', planDigest),
|
||||
});
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async function unlockModel(
|
||||
plan: Awaited<ReturnType<typeof durablePlan>>,
|
||||
completion: Parameters<
|
||||
typeof createCopilotFailureDiagnosisToolUnlockCommand
|
||||
>[0]['completion'],
|
||||
dependencies: CopilotFailureDiagnosisToolExecutionDependencies,
|
||||
) {
|
||||
let existing;
|
||||
try {
|
||||
existing = await dependencies.unlocks.findByRequestId(plan.requestId);
|
||||
} catch (cause) {
|
||||
return unavailable(cause);
|
||||
}
|
||||
if (existing) {
|
||||
if (
|
||||
existing.planDigest !== plan.planDigest ||
|
||||
existing.toolCompletionDigest !== completion.completionDigest
|
||||
) {
|
||||
throw new CopilotFailureDiagnosisToolExecutionConflictError(
|
||||
'the durable model unlock changed',
|
||||
);
|
||||
}
|
||||
return Object.freeze({ status: 'existing' as const, receipt: existing });
|
||||
}
|
||||
let modelStepRun;
|
||||
let run;
|
||||
try {
|
||||
[modelStepRun, run] = await Promise.all([
|
||||
dependencies.stepRuns.findById(plan.modelStepRunId),
|
||||
dependencies.runs.findRunById(plan.runId),
|
||||
]);
|
||||
} catch (cause) {
|
||||
return unavailable(cause);
|
||||
}
|
||||
if (!modelStepRun || !run) return unavailable();
|
||||
const command = createCopilotFailureDiagnosisToolUnlockCommand({
|
||||
plan,
|
||||
completion,
|
||||
modelStepRun,
|
||||
run: {
|
||||
id: run.id,
|
||||
projectId: run.projectId,
|
||||
status: run.status,
|
||||
version: run.version,
|
||||
eventSequence: run.eventSequence,
|
||||
},
|
||||
});
|
||||
try {
|
||||
return await dependencies.unlocks.commit(command);
|
||||
} catch (cause) {
|
||||
let recovered;
|
||||
try {
|
||||
recovered = await dependencies.unlocks.findByRequestId(plan.requestId);
|
||||
} catch {
|
||||
throw cause;
|
||||
}
|
||||
if (
|
||||
recovered &&
|
||||
recovered.planDigest === plan.planDigest &&
|
||||
recovered.toolCompletionDigest === completion.completionDigest
|
||||
) {
|
||||
return Object.freeze({
|
||||
status: 'existing' as const,
|
||||
receipt: recovered,
|
||||
});
|
||||
}
|
||||
throw cause;
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeCopilotFailureDiagnosisTool(
|
||||
input: ExecuteCopilotFailureDiagnosisToolInput,
|
||||
dependencies: CopilotFailureDiagnosisToolExecutionDependencies,
|
||||
): Promise<Readonly<CopilotFailureDiagnosisToolExecutionResult>> {
|
||||
assertDependencies(dependencies);
|
||||
if (
|
||||
!input ||
|
||||
typeof input !== 'object' ||
|
||||
Array.isArray(input) ||
|
||||
typeof input.requestId !== 'string' ||
|
||||
typeof input.authorizer?.authorize !== 'function'
|
||||
) {
|
||||
return invalid('input is invalid');
|
||||
}
|
||||
const plan = await durablePlan(input.requestId, dependencies);
|
||||
const authority = await currentAuthority(plan, dependencies);
|
||||
const startId = identity('cds', plan.planDigest);
|
||||
const barrier = await prepareStart(
|
||||
plan,
|
||||
input,
|
||||
authority,
|
||||
dependencies,
|
||||
startId,
|
||||
);
|
||||
const definitions = authority.bindings.definitionRegistry();
|
||||
const adapters = new TrustedToolExecutionAdapterRegistry(authority.bindings, [
|
||||
new BuiltInRunLogExcerptToolAdapter(
|
||||
authority.binding,
|
||||
'cluster-control',
|
||||
definitions,
|
||||
dependencies.logs,
|
||||
),
|
||||
]);
|
||||
const ids = completionIdentities(plan.planDigest);
|
||||
const completed = await executeAndCompleteTrustedTool(barrier.startId, {
|
||||
barriers: dependencies.barriers,
|
||||
artifacts: dependencies.artifacts,
|
||||
keys: dependencies.invocationKeys,
|
||||
adapters,
|
||||
completions: dependencies.completions,
|
||||
failureCompletions: dependencies.failureCompletions,
|
||||
stepRuns: dependencies.stepRuns,
|
||||
runs: dependencies.runs,
|
||||
resultKeyCatalog: dependencies.resultKeyCatalog,
|
||||
resultRekeys: dependencies.resultRekeys,
|
||||
resultKeys: dependencies.resultKeys,
|
||||
identities: ids.success,
|
||||
failureIdentities: ids.failure,
|
||||
...(dependencies.now === undefined ? {} : { now: dependencies.now }),
|
||||
...(dependencies.nonceFactory === undefined
|
||||
? {}
|
||||
: { nonceFactory: dependencies.nonceFactory }),
|
||||
});
|
||||
if (completed.outcome !== 'succeeded') {
|
||||
return Object.freeze({
|
||||
outcome: completed.outcome,
|
||||
completionStatus: completed.status,
|
||||
unlockStatus: null,
|
||||
});
|
||||
}
|
||||
const unlock = await unlockModel(plan, completed.completion, dependencies);
|
||||
return Object.freeze({
|
||||
outcome: 'succeeded' as const,
|
||||
completionStatus: completed.status,
|
||||
unlockStatus: unlock.status,
|
||||
completion: completed.completion,
|
||||
unlock: unlock.receipt,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
BUILTIN_RUN_LOG_EXCERPT_TIMEOUT_SECONDS,
|
||||
BUILTIN_RUN_LOG_EXCERPT_TOOL,
|
||||
createBuiltInRunLogExcerptToolHandlerBinding,
|
||||
} from '@qinglong/runtime-core/builtin-run-log-excerpt-tool';
|
||||
import {
|
||||
normalizeProjectToolDefinitionSnapshot,
|
||||
type ProjectToolDefinitionSnapshot,
|
||||
} from '@qinglong/runtime-core/project-tool-definition-snapshot';
|
||||
import {
|
||||
TrustedToolHandlerBindingRegistry,
|
||||
normalizeTrustedToolInvocationPlan,
|
||||
type TrustedToolHandlerBinding,
|
||||
type TrustedToolInvocationPlan,
|
||||
} from '@qinglong/runtime-core/trusted-tool-invocation';
|
||||
|
||||
import type { CopilotFailureDiagnosisExecutionPlan } from '../admission/contracts';
|
||||
import { normalizeCopilotFailureDiagnosisExecutionPlan } from '../admission/plan';
|
||||
import {
|
||||
CopilotFailureDiagnosisToolExecutionConflictError,
|
||||
InvalidCopilotFailureDiagnosisToolExecutionError,
|
||||
} from './contracts';
|
||||
|
||||
export interface CopilotFailureDiagnosisTrustedToolAuthority {
|
||||
readonly plan: Readonly<TrustedToolInvocationPlan>;
|
||||
readonly binding: Readonly<TrustedToolHandlerBinding>;
|
||||
readonly bindings: TrustedToolHandlerBindingRegistry;
|
||||
}
|
||||
|
||||
function invalid(message: string): never {
|
||||
throw new InvalidCopilotFailureDiagnosisToolExecutionError(message);
|
||||
}
|
||||
|
||||
export function restoreCopilotFailureDiagnosisTrustedToolAuthority(
|
||||
executionPlanValue: CopilotFailureDiagnosisExecutionPlan,
|
||||
snapshotValue: ProjectToolDefinitionSnapshot,
|
||||
): Readonly<CopilotFailureDiagnosisTrustedToolAuthority> {
|
||||
const executionPlan =
|
||||
normalizeCopilotFailureDiagnosisExecutionPlan(executionPlanValue);
|
||||
const snapshot = normalizeProjectToolDefinitionSnapshot(snapshotValue);
|
||||
if (
|
||||
snapshot.projectId !== executionPlan.projectId ||
|
||||
snapshot.snapshotDigest !== executionPlan.tool.snapshotDigest
|
||||
) {
|
||||
throw new CopilotFailureDiagnosisToolExecutionConflictError(
|
||||
'the current Project Tool snapshot changed',
|
||||
);
|
||||
}
|
||||
const binding = createBuiltInRunLogExcerptToolHandlerBinding(snapshot, [
|
||||
'cluster-control',
|
||||
]);
|
||||
if (
|
||||
binding.bindingDigest !== executionPlan.tool.bindingDigest ||
|
||||
binding.definitionDigest !== executionPlan.tool.definitionDigest
|
||||
) {
|
||||
throw new CopilotFailureDiagnosisToolExecutionConflictError(
|
||||
'the reviewed log Tool binding changed',
|
||||
);
|
||||
}
|
||||
const bindings = new TrustedToolHandlerBindingRegistry(snapshot, [binding]);
|
||||
let plan: Readonly<TrustedToolInvocationPlan>;
|
||||
try {
|
||||
plan = normalizeTrustedToolInvocationPlan(
|
||||
{
|
||||
schema: 'qinglong/trusted-tool-invocation-plan@v1',
|
||||
status: 'ready',
|
||||
actionType: 'tool.invoke',
|
||||
actionRef: executionPlan.tool.actionRef,
|
||||
projectId: executionPlan.projectId,
|
||||
requestedBy: executionPlan.requestedBySubject,
|
||||
tool: BUILTIN_RUN_LOG_EXCERPT_TOOL,
|
||||
permission: 'tool.call:qinglong.run.log.excerpt',
|
||||
requiredPermissions: ['artifact.read'],
|
||||
effect: 'read',
|
||||
risk: 'medium',
|
||||
policyFence: executionPlan.policyFence,
|
||||
profile: 'cluster-control',
|
||||
snapshotDigest: executionPlan.tool.snapshotDigest,
|
||||
definitionDigest: executionPlan.tool.definitionDigest,
|
||||
binding,
|
||||
timeoutSeconds: BUILTIN_RUN_LOG_EXCERPT_TIMEOUT_SECONDS,
|
||||
invocationArtifact: executionPlan.tool.invocationArtifact,
|
||||
invocationActionDigest: executionPlan.tool.invocationActionDigest,
|
||||
previewArtifact: executionPlan.tool.previewArtifact,
|
||||
actionDigest: executionPlan.tool.actionDigest,
|
||||
sealedAtMs: executionPlan.tool.sealedAtMs,
|
||||
planDigest: executionPlan.tool.planDigest,
|
||||
},
|
||||
bindings,
|
||||
);
|
||||
} catch (cause) {
|
||||
if (cause instanceof CopilotFailureDiagnosisToolExecutionConflictError) {
|
||||
throw cause;
|
||||
}
|
||||
return invalid('the admitted trusted Tool plan cannot be restored');
|
||||
}
|
||||
if (plan.planDigest !== executionPlan.tool.planDigest) {
|
||||
return invalid('the restored trusted Tool plan digest changed');
|
||||
}
|
||||
return Object.freeze({ plan, binding, bindings });
|
||||
}
|
||||
+548
@@ -0,0 +1,548 @@
|
||||
import { isDeepStrictEqual } from 'node:util';
|
||||
|
||||
import type {
|
||||
PostgresClient,
|
||||
PostgresPool,
|
||||
PostgresQueryable,
|
||||
} from '@qinglong/runtime-core';
|
||||
import {
|
||||
normalizeToolExecutionCompletionRecord,
|
||||
type ToolExecutionCompletionRecord,
|
||||
} from '@qinglong/runtime-core/tool-execution-completion';
|
||||
|
||||
import { POSTGRES_MODEL_INVOCATION_SCHEMA } from '../../../migration/modelInvocationMigration';
|
||||
import type { CopilotFailureDiagnosisExecutionPlan } from '../admission/contracts';
|
||||
import { normalizeCopilotFailureDiagnosisExecutionPlan } from '../admission/plan';
|
||||
import {
|
||||
CopilotFailureDiagnosisToolExecutionConflictError,
|
||||
CopilotFailureDiagnosisToolExecutionUnavailableError,
|
||||
type CopilotFailureDiagnosisToolUnlockCommand,
|
||||
type CopilotFailureDiagnosisToolUnlockReceipt,
|
||||
type CopilotFailureDiagnosisToolUnlockRepository,
|
||||
} from './contracts';
|
||||
import {
|
||||
normalizeCopilotFailureDiagnosisToolUnlockCommand,
|
||||
normalizeCopilotFailureDiagnosisToolUnlockReceipt,
|
||||
} from './unlockProtocol';
|
||||
|
||||
const UNLOCK_TABLE = 'copilot_failure_diagnosis_tool_unlocks';
|
||||
const RETRYABLE_SQL_STATES = new Set(['40001', '40P01']);
|
||||
const MAX_TRANSACTION_ATTEMPTS = 3;
|
||||
|
||||
type Row = Readonly<Record<string, unknown>>;
|
||||
|
||||
function unavailable(
|
||||
cause?: unknown,
|
||||
): CopilotFailureDiagnosisToolExecutionUnavailableError {
|
||||
return new CopilotFailureDiagnosisToolExecutionUnavailableError({
|
||||
cause: cause instanceof Error ? cause : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function sqlState(error: unknown): string | undefined {
|
||||
if (!error || typeof error !== 'object') return undefined;
|
||||
const value = (error as { code?: unknown }).code;
|
||||
return typeof value === 'string' ? value : undefined;
|
||||
}
|
||||
|
||||
function mapStorageError(error: unknown): Error {
|
||||
if (
|
||||
error instanceof CopilotFailureDiagnosisToolExecutionConflictError ||
|
||||
error instanceof CopilotFailureDiagnosisToolExecutionUnavailableError
|
||||
) {
|
||||
return error;
|
||||
}
|
||||
if (['23503', '23505', '23514'].includes(sqlState(error) ?? '')) {
|
||||
return new CopilotFailureDiagnosisToolExecutionConflictError(
|
||||
'a durable Tool unlock identity is already bound',
|
||||
);
|
||||
}
|
||||
return unavailable(error);
|
||||
}
|
||||
|
||||
function text(row: Row, key: string): string {
|
||||
const value = row[key];
|
||||
if (typeof value !== 'string' || value.length === 0) throw unavailable();
|
||||
return value;
|
||||
}
|
||||
|
||||
function integer(row: Row, key: string): number {
|
||||
const value = row[key];
|
||||
if (typeof value === 'number' && Number.isSafeInteger(value)) return value;
|
||||
if (typeof value === 'string' && /^(0|[1-9]\d*)$/.test(value)) {
|
||||
const parsed = Number(value);
|
||||
if (Number.isSafeInteger(parsed)) return parsed;
|
||||
}
|
||||
throw unavailable();
|
||||
}
|
||||
|
||||
function jsonObject(value: unknown): Record<string, unknown> {
|
||||
let parsed = value;
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
parsed = JSON.parse(value);
|
||||
} catch (cause) {
|
||||
throw unavailable(cause);
|
||||
}
|
||||
}
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
throw unavailable();
|
||||
}
|
||||
return parsed as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function same(left: unknown, right: unknown): boolean {
|
||||
return isDeepStrictEqual(left, right);
|
||||
}
|
||||
|
||||
async function begin(client: PostgresClient): Promise<void> {
|
||||
await client.query('BEGIN ISOLATION LEVEL SERIALIZABLE');
|
||||
await client.query(`SELECT set_config('statement_timeout', $1, true)`, [
|
||||
'5s',
|
||||
]);
|
||||
await client.query(`SELECT set_config('lock_timeout', $1, true)`, ['2s']);
|
||||
await client.query(
|
||||
`SELECT set_config('idle_in_transaction_session_timeout', $1, true)`,
|
||||
['5s'],
|
||||
);
|
||||
}
|
||||
|
||||
async function rollback(client: PostgresClient): Promise<void> {
|
||||
try {
|
||||
await client.query('ROLLBACK');
|
||||
} catch {
|
||||
// Preserve the original transaction failure.
|
||||
}
|
||||
}
|
||||
|
||||
type StoredUnlock = Readonly<{
|
||||
receipt: Readonly<CopilotFailureDiagnosisToolUnlockReceipt>;
|
||||
commandDigest: string;
|
||||
}>;
|
||||
|
||||
const UNLOCK_SELECT = `
|
||||
unlock.request_id AS "requestId",
|
||||
unlock.plan_digest AS "planDigest",
|
||||
unlock.start_id AS "startId",
|
||||
unlock.tool_completion_digest AS "toolCompletionDigest",
|
||||
unlock.model_step_run_id AS "modelStepRunId",
|
||||
unlock.model_step_run_version AS "modelStepRunVersion",
|
||||
unlock.model_step_run_digest AS "modelStepRunDigest",
|
||||
unlock.model_mutation_id AS "modelMutationId",
|
||||
unlock.model_mutation_digest AS "modelMutationDigest",
|
||||
unlock.model_event_id AS "modelEventId",
|
||||
unlock.final_run_version AS "finalRunVersion",
|
||||
unlock.final_run_event_sequence AS "finalRunEventSequence",
|
||||
unlock.unlocked_at_ms AS "unlockedAtMs",
|
||||
unlock.receipt_digest AS "receiptDigest",
|
||||
unlock.command_digest AS "commandDigest",
|
||||
unlock.receipt_json AS "receiptJson",
|
||||
admission.plan_digest AS "joinedPlanDigest",
|
||||
completion.completion_digest AS "joinedCompletionDigest",
|
||||
mutation.mutation_digest AS "joinedMutationDigest",
|
||||
mutation.step_run_digest AS "joinedModelStepRunDigest",
|
||||
mutation.event_id AS "joinedEventId",
|
||||
mutation.event_sequence AS "joinedEventSequence",
|
||||
mutation.run_version AS "joinedRunVersion"`;
|
||||
|
||||
async function findRows(
|
||||
queryable: PostgresQueryable,
|
||||
where: string,
|
||||
values: readonly unknown[],
|
||||
): Promise<readonly Row[]> {
|
||||
const result = await queryable.query<Row>(
|
||||
`SELECT ${UNLOCK_SELECT}
|
||||
FROM "${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${UNLOCK_TABLE}" AS unlock
|
||||
JOIN "${POSTGRES_MODEL_INVOCATION_SCHEMA}".
|
||||
"copilot_failure_diagnosis_admissions" AS admission
|
||||
ON admission.request_id = unlock.request_id
|
||||
JOIN "ql3"."tool_execution_completions" AS completion
|
||||
ON completion.start_id = unlock.start_id
|
||||
JOIN "ql3"."step_run_mutations" AS mutation
|
||||
ON mutation.mutation_id = unlock.model_mutation_id
|
||||
WHERE ${where}
|
||||
LIMIT 2`,
|
||||
values,
|
||||
);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
function storedUnlock(row: Row): StoredUnlock {
|
||||
let receipt: Readonly<CopilotFailureDiagnosisToolUnlockReceipt>;
|
||||
try {
|
||||
receipt = normalizeCopilotFailureDiagnosisToolUnlockReceipt(
|
||||
jsonObject(
|
||||
row.receiptJson,
|
||||
) as unknown as CopilotFailureDiagnosisToolUnlockReceipt,
|
||||
);
|
||||
} catch (cause) {
|
||||
throw unavailable(cause);
|
||||
}
|
||||
if (
|
||||
text(row, 'requestId') !== receipt.requestId ||
|
||||
text(row, 'planDigest') !== receipt.planDigest ||
|
||||
text(row, 'startId') !== receipt.startId ||
|
||||
text(row, 'toolCompletionDigest') !== receipt.toolCompletionDigest ||
|
||||
text(row, 'modelStepRunId') !== receipt.modelStepRunId ||
|
||||
integer(row, 'modelStepRunVersion') !== receipt.modelStepRunVersion ||
|
||||
text(row, 'modelStepRunDigest') !== receipt.modelStepRunDigest ||
|
||||
text(row, 'modelMutationId') !== receipt.modelMutationId ||
|
||||
text(row, 'modelMutationDigest') !== receipt.modelMutationDigest ||
|
||||
text(row, 'modelEventId') !== receipt.modelEventId ||
|
||||
integer(row, 'finalRunVersion') !== receipt.finalRunVersion ||
|
||||
integer(row, 'finalRunEventSequence') !== receipt.finalRunEventSequence ||
|
||||
integer(row, 'unlockedAtMs') !== receipt.unlockedAtMs ||
|
||||
text(row, 'receiptDigest') !== receipt.receiptDigest ||
|
||||
text(row, 'joinedPlanDigest') !== receipt.planDigest ||
|
||||
text(row, 'joinedCompletionDigest') !== receipt.toolCompletionDigest ||
|
||||
text(row, 'joinedMutationDigest') !== receipt.modelMutationDigest ||
|
||||
text(row, 'joinedModelStepRunDigest') !== receipt.modelStepRunDigest ||
|
||||
text(row, 'joinedEventId') !== receipt.modelEventId ||
|
||||
integer(row, 'joinedEventSequence') !== receipt.finalRunEventSequence ||
|
||||
integer(row, 'joinedRunVersion') !== receipt.finalRunVersion
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
return Object.freeze({
|
||||
receipt,
|
||||
commandDigest: text(row, 'commandDigest'),
|
||||
});
|
||||
}
|
||||
|
||||
async function updateModelStepRun(
|
||||
client: PostgresClient,
|
||||
command: Readonly<CopilotFailureDiagnosisToolUnlockCommand>,
|
||||
): Promise<void> {
|
||||
const mutation = command.modelStepRunMutation;
|
||||
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 CopilotFailureDiagnosisToolExecutionConflictError(
|
||||
'the Model StepRun unlock fence changed',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateRun(
|
||||
client: PostgresClient,
|
||||
command: Readonly<CopilotFailureDiagnosisToolUnlockCommand>,
|
||||
): Promise<void> {
|
||||
const mutation = command.modelStepRunMutation;
|
||||
const result = await client.query(
|
||||
`UPDATE "ql3"."runs"
|
||||
SET version = version + 1, event_sequence = event_sequence + 1
|
||||
WHERE id = $1 AND status = 'running'
|
||||
AND version = $2 AND event_sequence = $3`,
|
||||
[
|
||||
mutation.runId,
|
||||
mutation.expectedRunVersion,
|
||||
mutation.expectedRunEventSequence,
|
||||
],
|
||||
);
|
||||
if (result.rowCount !== 1) {
|
||||
throw new CopilotFailureDiagnosisToolExecutionConflictError(
|
||||
'the diagnosis Run unlock fence changed',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function insertEventAndMutation(
|
||||
client: PostgresClient,
|
||||
command: Readonly<CopilotFailureDiagnosisToolUnlockCommand>,
|
||||
): Promise<void> {
|
||||
const mutation = command.modelStepRunMutation;
|
||||
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,
|
||||
],
|
||||
);
|
||||
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 insertUnlock(
|
||||
client: PostgresClient,
|
||||
command: Readonly<CopilotFailureDiagnosisToolUnlockCommand>,
|
||||
): Promise<void> {
|
||||
const receipt = command.receipt;
|
||||
await client.query(
|
||||
`INSERT INTO "${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${UNLOCK_TABLE}" (
|
||||
request_id, plan_digest, run_id, start_id, tool_step_run_id,
|
||||
tool_completion_digest,
|
||||
model_step_run_id, model_step_run_version, model_step_run_digest,
|
||||
model_mutation_id, model_mutation_digest, model_event_id,
|
||||
final_run_version, final_run_event_sequence, unlocked_at_ms,
|
||||
receipt_digest, command_digest, receipt_json
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
|
||||
$14, $15, $16, $17, $18::jsonb
|
||||
)`,
|
||||
[
|
||||
receipt.requestId,
|
||||
receipt.planDigest,
|
||||
receipt.runId,
|
||||
receipt.startId,
|
||||
receipt.toolStepRunId,
|
||||
receipt.toolCompletionDigest,
|
||||
receipt.modelStepRunId,
|
||||
receipt.modelStepRunVersion,
|
||||
receipt.modelStepRunDigest,
|
||||
receipt.modelMutationId,
|
||||
receipt.modelMutationDigest,
|
||||
receipt.modelEventId,
|
||||
receipt.finalRunVersion,
|
||||
receipt.finalRunEventSequence,
|
||||
receipt.unlockedAtMs,
|
||||
receipt.receiptDigest,
|
||||
command.commandDigest,
|
||||
JSON.stringify(receipt),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
async function currentEvidence(
|
||||
client: PostgresClient,
|
||||
command: Readonly<CopilotFailureDiagnosisToolUnlockCommand>,
|
||||
): Promise<void> {
|
||||
const mutation = command.modelStepRunMutation;
|
||||
const result = await client.query<Row>(
|
||||
`SELECT admission.plan_json AS "planJson",
|
||||
completion.completion_json AS "completionJson",
|
||||
model_step.kind AS "modelKind",
|
||||
model_step.status AS "modelStatus",
|
||||
model_step.version AS "modelVersion",
|
||||
model_step.step_run_digest AS "modelDigest",
|
||||
run.project_id AS "projectId", run.status AS "runStatus",
|
||||
run.version AS "runVersion",
|
||||
run.event_sequence AS "runEventSequence"
|
||||
FROM "${POSTGRES_MODEL_INVOCATION_SCHEMA}".
|
||||
"copilot_failure_diagnosis_admissions" AS admission
|
||||
JOIN "ql3"."tool_execution_completions" AS completion
|
||||
ON completion.start_id = $2
|
||||
AND completion.run_id = admission.run_id
|
||||
AND completion.step_run_id = admission.tool_step_run_id
|
||||
JOIN "ql3"."step_runs" AS model_step
|
||||
ON model_step.run_id = admission.run_id
|
||||
AND model_step.id = admission.model_step_run_id
|
||||
JOIN "ql3"."runs" AS run ON run.id = admission.run_id
|
||||
WHERE admission.request_id = $1
|
||||
LIMIT 2
|
||||
FOR UPDATE OF model_step, run`,
|
||||
[command.plan.requestId, command.completion.startId],
|
||||
);
|
||||
const row = result.rows[0];
|
||||
let plan: Readonly<CopilotFailureDiagnosisExecutionPlan>;
|
||||
let completion: Readonly<ToolExecutionCompletionRecord>;
|
||||
try {
|
||||
plan = row
|
||||
? normalizeCopilotFailureDiagnosisExecutionPlan(
|
||||
jsonObject(
|
||||
row.planJson,
|
||||
) as unknown as CopilotFailureDiagnosisExecutionPlan,
|
||||
)
|
||||
: command.plan;
|
||||
completion = row
|
||||
? normalizeToolExecutionCompletionRecord(
|
||||
jsonObject(
|
||||
row.completionJson,
|
||||
) as unknown as ToolExecutionCompletionRecord,
|
||||
)
|
||||
: command.completion;
|
||||
} catch (cause) {
|
||||
throw unavailable(cause);
|
||||
}
|
||||
if (
|
||||
result.rows.length !== 1 ||
|
||||
!row ||
|
||||
!same(plan, command.plan) ||
|
||||
!same(completion, command.completion) ||
|
||||
text(row, 'modelKind') !== 'model' ||
|
||||
text(row, 'modelStatus') !== mutation.previousStatus ||
|
||||
integer(row, 'modelVersion') !== mutation.expectedStepRunVersion ||
|
||||
text(row, 'modelDigest') !== mutation.expectedStepRunDigest ||
|
||||
text(row, 'projectId') !== command.plan.projectId ||
|
||||
text(row, 'runStatus') !== 'running' ||
|
||||
integer(row, 'runVersion') !== mutation.expectedRunVersion ||
|
||||
integer(row, 'runEventSequence') !== mutation.expectedRunEventSequence
|
||||
) {
|
||||
throw new CopilotFailureDiagnosisToolExecutionConflictError(
|
||||
'the admitted Tool completion or Model StepRun fence changed',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class PostgresCopilotFailureDiagnosisToolUnlockRepository
|
||||
implements CopilotFailureDiagnosisToolUnlockRepository
|
||||
{
|
||||
constructor(private readonly pool: PostgresPool) {
|
||||
if (
|
||||
!pool ||
|
||||
typeof pool.query !== 'function' ||
|
||||
typeof pool.connect !== 'function'
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
async findByRequestId(
|
||||
requestId: string,
|
||||
): Promise<Readonly<CopilotFailureDiagnosisToolUnlockReceipt> | null> {
|
||||
if (
|
||||
typeof requestId !== 'string' ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(requestId)
|
||||
) {
|
||||
throw new TypeError('Tool unlock request id is invalid');
|
||||
}
|
||||
try {
|
||||
const rows = await findRows(this.pool, 'unlock.request_id = $1', [
|
||||
requestId,
|
||||
]);
|
||||
if (rows.length > 1) throw unavailable();
|
||||
return rows[0] ? storedUnlock(rows[0]).receipt : null;
|
||||
} catch (error) {
|
||||
throw mapStorageError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async commit(commandValue: CopilotFailureDiagnosisToolUnlockCommand): Promise<
|
||||
Readonly<{
|
||||
status: 'created' | 'existing';
|
||||
receipt: Readonly<CopilotFailureDiagnosisToolUnlockReceipt>;
|
||||
}>
|
||||
> {
|
||||
const command =
|
||||
normalizeCopilotFailureDiagnosisToolUnlockCommand(commandValue);
|
||||
for (let attempt = 0; attempt < MAX_TRANSACTION_ATTEMPTS; attempt += 1) {
|
||||
let client: PostgresClient;
|
||||
try {
|
||||
client = await this.pool.connect();
|
||||
} catch (cause) {
|
||||
throw unavailable(cause);
|
||||
}
|
||||
let began = false;
|
||||
try {
|
||||
await begin(client);
|
||||
began = true;
|
||||
const existingRows = await findRows(
|
||||
client,
|
||||
`unlock.request_id = $1
|
||||
OR unlock.tool_completion_digest = $2
|
||||
OR unlock.model_mutation_id = $3
|
||||
OR unlock.model_event_id = $4`,
|
||||
[
|
||||
command.receipt.requestId,
|
||||
command.receipt.toolCompletionDigest,
|
||||
command.receipt.modelMutationId,
|
||||
command.receipt.modelEventId,
|
||||
],
|
||||
);
|
||||
if (existingRows.length > 1) {
|
||||
throw new CopilotFailureDiagnosisToolExecutionConflictError();
|
||||
}
|
||||
if (existingRows[0]) {
|
||||
const stored = storedUnlock(existingRows[0]);
|
||||
if (
|
||||
stored.commandDigest !== command.commandDigest ||
|
||||
!same(stored.receipt, command.receipt)
|
||||
) {
|
||||
throw new CopilotFailureDiagnosisToolExecutionConflictError();
|
||||
}
|
||||
await client.query('COMMIT');
|
||||
began = false;
|
||||
return Object.freeze({
|
||||
status: 'existing' as const,
|
||||
receipt: stored.receipt,
|
||||
});
|
||||
}
|
||||
|
||||
await currentEvidence(client, command);
|
||||
await updateModelStepRun(client, command);
|
||||
await updateRun(client, command);
|
||||
await insertEventAndMutation(client, command);
|
||||
await insertUnlock(client, command);
|
||||
await client.query('COMMIT');
|
||||
began = false;
|
||||
return Object.freeze({
|
||||
status: 'created' as const,
|
||||
receipt: command.receipt,
|
||||
});
|
||||
} catch (error) {
|
||||
if (began) await rollback(client);
|
||||
if (
|
||||
RETRYABLE_SQL_STATES.has(sqlState(error) ?? '') &&
|
||||
attempt + 1 < MAX_TRANSACTION_ATTEMPTS
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
throw mapStorageError(error);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
import {
|
||||
normalizeStepRunMutation,
|
||||
normalizeStepRunRecord,
|
||||
transitionStepRunMutation,
|
||||
type StepRunRecord,
|
||||
} from '@qinglong/runtime-core/step-run';
|
||||
import {
|
||||
normalizeToolExecutionCompletionRecord,
|
||||
type ToolExecutionCompletionRecord,
|
||||
} from '@qinglong/runtime-core/tool-execution-completion';
|
||||
|
||||
import type { CopilotFailureDiagnosisExecutionPlan } from '../admission/contracts';
|
||||
import { normalizeCopilotFailureDiagnosisExecutionPlan } from '../admission/plan';
|
||||
import {
|
||||
COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_COMMAND_SCHEMA,
|
||||
COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_RECEIPT_SCHEMA,
|
||||
InvalidCopilotFailureDiagnosisToolExecutionError,
|
||||
MAX_COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_COMMAND_BYTES,
|
||||
MAX_COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_RECEIPT_BYTES,
|
||||
type CopilotFailureDiagnosisRunAuthority,
|
||||
type CopilotFailureDiagnosisToolUnlockCommand,
|
||||
type CopilotFailureDiagnosisToolUnlockReceipt,
|
||||
} from './contracts';
|
||||
|
||||
const RECEIPT_DIGEST_DOMAIN = Buffer.from(
|
||||
'qinglong/copilot-failure-diagnosis-tool-unlock-receipt-digest@v1\0',
|
||||
'utf8',
|
||||
);
|
||||
const COMMAND_DIGEST_DOMAIN = Buffer.from(
|
||||
'qinglong/copilot-failure-diagnosis-tool-unlock-command-digest@v1\0',
|
||||
'utf8',
|
||||
);
|
||||
const IDENTITY_DOMAIN = Buffer.from(
|
||||
'qinglong/copilot-failure-diagnosis-tool-unlock-identity@v1\0',
|
||||
'utf8',
|
||||
);
|
||||
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
|
||||
const IDENTITY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
|
||||
function invalid(message: string): never {
|
||||
throw new InvalidCopilotFailureDiagnosisToolExecutionError(message);
|
||||
}
|
||||
|
||||
function hash(domain: Uint8Array, value: unknown): string {
|
||||
return createHash('sha256')
|
||||
.update(domain)
|
||||
.update(JSON.stringify(value))
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
function dataRecord(value: unknown, label: string): Record<string, unknown> {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
Object.getPrototypeOf(value) !== Object.prototype
|
||||
) {
|
||||
return invalid(`${label} must be a plain object`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function exactKeys(
|
||||
value: object,
|
||||
keys: readonly string[],
|
||||
label: string,
|
||||
): void {
|
||||
const actual = Object.keys(value).sort();
|
||||
const expected = [...keys].sort();
|
||||
if (
|
||||
actual.length !== expected.length ||
|
||||
actual.some((key, index) => key !== expected[index])
|
||||
) {
|
||||
invalid(`${label} shape is invalid`);
|
||||
}
|
||||
}
|
||||
|
||||
function identity(value: unknown, label: string): string {
|
||||
if (typeof value !== 'string' || !IDENTITY_PATTERN.test(value)) {
|
||||
return invalid(`${label} is invalid`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function digest(value: unknown, label: string): string {
|
||||
if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) {
|
||||
return invalid(`${label} is invalid`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function integer(value: unknown, minimum: number, label: string): number {
|
||||
if (!Number.isSafeInteger(value) || (value as number) < minimum) {
|
||||
return invalid(`${label} is invalid`);
|
||||
}
|
||||
return value as number;
|
||||
}
|
||||
|
||||
function evidenceIdentity(
|
||||
prefix: 'cdum' | 'cdue',
|
||||
planDigest: string,
|
||||
completionDigest: string,
|
||||
): string {
|
||||
const maximumDigestLength = 35 - prefix.length;
|
||||
return `${prefix}:${hash(IDENTITY_DOMAIN, {
|
||||
prefix,
|
||||
planDigest,
|
||||
completionDigest,
|
||||
}).slice(0, maximumDigestLength)}`;
|
||||
}
|
||||
|
||||
export function copilotFailureDiagnosisToolUnlockReceiptDigest(
|
||||
value: Omit<CopilotFailureDiagnosisToolUnlockReceipt, 'receiptDigest'>,
|
||||
): string {
|
||||
return hash(RECEIPT_DIGEST_DOMAIN, value);
|
||||
}
|
||||
|
||||
export function normalizeCopilotFailureDiagnosisToolUnlockReceipt(
|
||||
value: CopilotFailureDiagnosisToolUnlockReceipt,
|
||||
): Readonly<CopilotFailureDiagnosisToolUnlockReceipt> {
|
||||
const candidate = dataRecord(value, 'Tool unlock receipt');
|
||||
exactKeys(
|
||||
candidate,
|
||||
[
|
||||
'barrierDigest',
|
||||
'finalRunEventSequence',
|
||||
'finalRunVersion',
|
||||
'modelEventId',
|
||||
'modelMutationDigest',
|
||||
'modelMutationId',
|
||||
'modelStepRunDigest',
|
||||
'modelStepRunId',
|
||||
'modelStepRunVersion',
|
||||
'planDigest',
|
||||
'receiptDigest',
|
||||
'requestId',
|
||||
'resultArtifact',
|
||||
'runId',
|
||||
'schema',
|
||||
'startId',
|
||||
'toolCompletionDigest',
|
||||
'toolStepRunId',
|
||||
'unlockedAtMs',
|
||||
],
|
||||
'Tool unlock receipt',
|
||||
);
|
||||
if (value.schema !== COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_RECEIPT_SCHEMA) {
|
||||
return invalid('Tool unlock receipt schema is unsupported');
|
||||
}
|
||||
const artifact = dataRecord(value.resultArtifact, 'result Artifact');
|
||||
exactKeys(
|
||||
artifact,
|
||||
['artifactDigest', 'artifactId', 'executionResultDigest', 'outputDigest'],
|
||||
'result Artifact',
|
||||
);
|
||||
const unsigned = Object.freeze({
|
||||
schema: COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_RECEIPT_SCHEMA,
|
||||
requestId: identity(value.requestId, 'request id'),
|
||||
planDigest: digest(value.planDigest, 'plan digest'),
|
||||
runId: identity(value.runId, 'Run id'),
|
||||
startId: identity(value.startId, 'start id'),
|
||||
barrierDigest: digest(value.barrierDigest, 'barrier digest'),
|
||||
toolStepRunId: identity(value.toolStepRunId, 'Tool StepRun id'),
|
||||
toolCompletionDigest: digest(
|
||||
value.toolCompletionDigest,
|
||||
'Tool completion digest',
|
||||
),
|
||||
resultArtifact: Object.freeze({
|
||||
artifactId: identity(value.resultArtifact.artifactId, 'Artifact id'),
|
||||
artifactDigest: digest(
|
||||
value.resultArtifact.artifactDigest,
|
||||
'Artifact digest',
|
||||
),
|
||||
outputDigest: digest(value.resultArtifact.outputDigest, 'output digest'),
|
||||
executionResultDigest: digest(
|
||||
value.resultArtifact.executionResultDigest,
|
||||
'execution result digest',
|
||||
),
|
||||
}),
|
||||
modelStepRunId: identity(value.modelStepRunId, 'model StepRun id'),
|
||||
modelStepRunVersion: integer(
|
||||
value.modelStepRunVersion,
|
||||
2,
|
||||
'model StepRun version',
|
||||
),
|
||||
modelStepRunDigest: digest(
|
||||
value.modelStepRunDigest,
|
||||
'model StepRun digest',
|
||||
),
|
||||
modelMutationId: identity(value.modelMutationId, 'model mutation id'),
|
||||
modelMutationDigest: digest(
|
||||
value.modelMutationDigest,
|
||||
'model mutation digest',
|
||||
),
|
||||
modelEventId: identity(value.modelEventId, 'model event id'),
|
||||
finalRunVersion: integer(value.finalRunVersion, 1, 'final Run version'),
|
||||
finalRunEventSequence: integer(
|
||||
value.finalRunEventSequence,
|
||||
1,
|
||||
'final Run event sequence',
|
||||
),
|
||||
unlockedAtMs: integer(value.unlockedAtMs, 0, 'unlock time'),
|
||||
} satisfies Omit<CopilotFailureDiagnosisToolUnlockReceipt, 'receiptDigest'>);
|
||||
if (unsigned.finalRunVersion !== unsigned.finalRunEventSequence) {
|
||||
return invalid('Tool unlock Run fence is invalid');
|
||||
}
|
||||
const receiptDigest = digest(value.receiptDigest, 'receipt digest');
|
||||
if (
|
||||
copilotFailureDiagnosisToolUnlockReceiptDigest(unsigned) !== receiptDigest
|
||||
) {
|
||||
return invalid('Tool unlock receipt digest does not match');
|
||||
}
|
||||
const normalized = Object.freeze({ ...unsigned, receiptDigest });
|
||||
if (
|
||||
Buffer.byteLength(JSON.stringify(normalized), 'utf8') >
|
||||
MAX_COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_RECEIPT_BYTES
|
||||
) {
|
||||
return invalid('Tool unlock receipt exceeds its byte budget');
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function commandUnsigned(
|
||||
value: Readonly<CopilotFailureDiagnosisToolUnlockCommand>,
|
||||
): Omit<CopilotFailureDiagnosisToolUnlockCommand, 'commandDigest'> {
|
||||
const { commandDigest: _commandDigest, ...unsigned } = value;
|
||||
return unsigned;
|
||||
}
|
||||
|
||||
function validateCommandBindings(
|
||||
plan: Readonly<CopilotFailureDiagnosisExecutionPlan>,
|
||||
completion: Readonly<ToolExecutionCompletionRecord>,
|
||||
mutation: ReturnType<typeof normalizeStepRunMutation>,
|
||||
receipt: Readonly<CopilotFailureDiagnosisToolUnlockReceipt>,
|
||||
): void {
|
||||
if (
|
||||
completion.projectId !== plan.projectId ||
|
||||
completion.runId !== plan.runId ||
|
||||
completion.stepRunId !== plan.toolStepRunId ||
|
||||
mutation.runId !== plan.runId ||
|
||||
mutation.previousStatus !== 'pending' ||
|
||||
mutation.stepRun.id !== plan.modelStepRunId ||
|
||||
mutation.stepRun.runId !== plan.runId ||
|
||||
mutation.stepRun.parentStepRunId !== plan.toolStepRunId ||
|
||||
mutation.stepRun.kind !== 'model' ||
|
||||
mutation.stepRun.status !== 'ready' ||
|
||||
mutation.stepRun.inputRef !== `tool-result-step:${plan.toolStepRunId}` ||
|
||||
mutation.stepRun.updatedAtMs !== completion.completedAtMs ||
|
||||
receipt.requestId !== plan.requestId ||
|
||||
receipt.planDigest !== plan.planDigest ||
|
||||
receipt.runId !== plan.runId ||
|
||||
receipt.startId !== completion.startId ||
|
||||
receipt.barrierDigest !== completion.barrierDigest ||
|
||||
receipt.toolStepRunId !== completion.stepRunId ||
|
||||
receipt.toolCompletionDigest !== completion.completionDigest ||
|
||||
JSON.stringify(receipt.resultArtifact) !==
|
||||
JSON.stringify(completion.resultArtifact) ||
|
||||
receipt.modelStepRunId !== mutation.stepRun.id ||
|
||||
receipt.modelStepRunVersion !== mutation.stepRun.version ||
|
||||
receipt.modelStepRunDigest !== mutation.stepRun.stepRunDigest ||
|
||||
receipt.modelMutationId !== mutation.mutationId ||
|
||||
receipt.modelMutationDigest !== mutation.mutationDigest ||
|
||||
receipt.modelEventId !== mutation.event.id ||
|
||||
receipt.finalRunVersion !== mutation.expectedRunVersion + 1 ||
|
||||
receipt.finalRunEventSequence !== mutation.expectedRunEventSequence + 1 ||
|
||||
receipt.unlockedAtMs !== mutation.stepRun.updatedAtMs
|
||||
) {
|
||||
return invalid('Tool unlock command bindings are inconsistent');
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeCopilotFailureDiagnosisToolUnlockCommand(
|
||||
value: CopilotFailureDiagnosisToolUnlockCommand,
|
||||
): Readonly<CopilotFailureDiagnosisToolUnlockCommand> {
|
||||
const candidate = dataRecord(value, 'Tool unlock command');
|
||||
exactKeys(
|
||||
candidate,
|
||||
[
|
||||
'commandDigest',
|
||||
'completion',
|
||||
'modelStepRunMutation',
|
||||
'plan',
|
||||
'receipt',
|
||||
'schema',
|
||||
],
|
||||
'Tool unlock command',
|
||||
);
|
||||
if (value.schema !== COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_COMMAND_SCHEMA) {
|
||||
return invalid('Tool unlock command schema is unsupported');
|
||||
}
|
||||
const plan = normalizeCopilotFailureDiagnosisExecutionPlan(value.plan);
|
||||
const completion = normalizeToolExecutionCompletionRecord(value.completion);
|
||||
const modelStepRunMutation = normalizeStepRunMutation(
|
||||
value.modelStepRunMutation,
|
||||
);
|
||||
const receipt = normalizeCopilotFailureDiagnosisToolUnlockReceipt(
|
||||
value.receipt,
|
||||
);
|
||||
validateCommandBindings(plan, completion, modelStepRunMutation, receipt);
|
||||
const unsigned = Object.freeze({
|
||||
schema: COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_COMMAND_SCHEMA,
|
||||
plan,
|
||||
completion,
|
||||
modelStepRunMutation,
|
||||
receipt,
|
||||
} satisfies Omit<CopilotFailureDiagnosisToolUnlockCommand, 'commandDigest'>);
|
||||
const commandDigest = digest(value.commandDigest, 'command digest');
|
||||
if (hash(COMMAND_DIGEST_DOMAIN, unsigned) !== commandDigest) {
|
||||
return invalid('Tool unlock command digest does not match');
|
||||
}
|
||||
const normalized = Object.freeze({ ...unsigned, commandDigest });
|
||||
if (
|
||||
Buffer.byteLength(JSON.stringify(normalized), 'utf8') >
|
||||
MAX_COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_COMMAND_BYTES
|
||||
) {
|
||||
return invalid('Tool unlock command exceeds its byte budget');
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function createCopilotFailureDiagnosisToolUnlockCommand(input: {
|
||||
readonly plan: CopilotFailureDiagnosisExecutionPlan;
|
||||
readonly completion: ToolExecutionCompletionRecord;
|
||||
readonly modelStepRun: StepRunRecord;
|
||||
readonly run: CopilotFailureDiagnosisRunAuthority;
|
||||
}): Readonly<CopilotFailureDiagnosisToolUnlockCommand> {
|
||||
const plan = normalizeCopilotFailureDiagnosisExecutionPlan(input.plan);
|
||||
const completion = normalizeToolExecutionCompletionRecord(input.completion);
|
||||
const modelStepRun = normalizeStepRunRecord(input.modelStepRun);
|
||||
const run = dataRecord(input.run, 'Run authority');
|
||||
exactKeys(
|
||||
run,
|
||||
['eventSequence', 'id', 'projectId', 'status', 'version'],
|
||||
'Run authority',
|
||||
);
|
||||
if (
|
||||
input.run.id !== plan.runId ||
|
||||
input.run.projectId !== plan.projectId ||
|
||||
input.run.status !== 'running' ||
|
||||
!Number.isSafeInteger(input.run.version) ||
|
||||
input.run.version < 1 ||
|
||||
!Number.isSafeInteger(input.run.eventSequence) ||
|
||||
input.run.eventSequence !== input.run.version ||
|
||||
completion.projectId !== plan.projectId ||
|
||||
completion.runId !== plan.runId ||
|
||||
completion.stepRunId !== plan.toolStepRunId ||
|
||||
modelStepRun.id !== plan.modelStepRunId ||
|
||||
modelStepRun.runId !== plan.runId ||
|
||||
modelStepRun.parentStepRunId !== plan.toolStepRunId ||
|
||||
modelStepRun.kind !== 'model' ||
|
||||
modelStepRun.status !== 'pending'
|
||||
) {
|
||||
return invalid('Tool completion cannot unlock the model StepRun');
|
||||
}
|
||||
const mutationId = evidenceIdentity(
|
||||
'cdum',
|
||||
plan.planDigest,
|
||||
completion.completionDigest,
|
||||
);
|
||||
const eventId = evidenceIdentity(
|
||||
'cdue',
|
||||
plan.planDigest,
|
||||
completion.completionDigest,
|
||||
);
|
||||
const modelStepRunMutation = transitionStepRunMutation(
|
||||
modelStepRun,
|
||||
{
|
||||
expectedVersion: modelStepRun.version,
|
||||
expectedDigest: modelStepRun.stepRunDigest,
|
||||
mutationId,
|
||||
to: 'ready',
|
||||
atMs: completion.completedAtMs,
|
||||
},
|
||||
{
|
||||
expectedRunVersion: input.run.version,
|
||||
expectedRunEventSequence: input.run.eventSequence,
|
||||
eventId,
|
||||
dedupeKey: eventId,
|
||||
actor: Object.freeze({
|
||||
type: 'system' as const,
|
||||
id: 'copilot-runtime',
|
||||
}),
|
||||
},
|
||||
);
|
||||
const receiptUnsigned = Object.freeze({
|
||||
schema: COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_RECEIPT_SCHEMA,
|
||||
requestId: plan.requestId,
|
||||
planDigest: plan.planDigest,
|
||||
runId: plan.runId,
|
||||
startId: completion.startId,
|
||||
barrierDigest: completion.barrierDigest,
|
||||
toolStepRunId: completion.stepRunId,
|
||||
toolCompletionDigest: completion.completionDigest,
|
||||
resultArtifact: completion.resultArtifact,
|
||||
modelStepRunId: modelStepRunMutation.stepRun.id,
|
||||
modelStepRunVersion: modelStepRunMutation.stepRun.version,
|
||||
modelStepRunDigest: modelStepRunMutation.stepRun.stepRunDigest,
|
||||
modelMutationId: modelStepRunMutation.mutationId,
|
||||
modelMutationDigest: modelStepRunMutation.mutationDigest,
|
||||
modelEventId: modelStepRunMutation.event.id,
|
||||
finalRunVersion: modelStepRunMutation.expectedRunVersion + 1,
|
||||
finalRunEventSequence: modelStepRunMutation.expectedRunEventSequence + 1,
|
||||
unlockedAtMs: completion.completedAtMs,
|
||||
} satisfies Omit<CopilotFailureDiagnosisToolUnlockReceipt, 'receiptDigest'>);
|
||||
const receipt = normalizeCopilotFailureDiagnosisToolUnlockReceipt({
|
||||
...receiptUnsigned,
|
||||
receiptDigest:
|
||||
copilotFailureDiagnosisToolUnlockReceiptDigest(receiptUnsigned),
|
||||
});
|
||||
const unsigned = Object.freeze({
|
||||
schema: COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_COMMAND_SCHEMA,
|
||||
plan,
|
||||
completion,
|
||||
modelStepRunMutation,
|
||||
receipt,
|
||||
} satisfies Omit<CopilotFailureDiagnosisToolUnlockCommand, 'commandDigest'>);
|
||||
return normalizeCopilotFailureDiagnosisToolUnlockCommand({
|
||||
...unsigned,
|
||||
commandDigest: hash(COMMAND_DIGEST_DOMAIN, unsigned),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './tool-execution/contracts';
|
||||
export * from './tool-execution/coordinator';
|
||||
export * from './tool-execution/planAuthority';
|
||||
export * from './tool-execution/unlockProtocol';
|
||||
@@ -63,6 +63,8 @@ export const POSTGRES_PLUGIN_PACKAGE_PROMPT_PRODUCT_AUTHORIZATION_MIGRATION_ID =
|
||||
'pg-9017-ai-plugin-package-prompt-product-authorization';
|
||||
export const POSTGRES_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_MIGRATION_ID =
|
||||
'pg-9018-ai-copilot-failure-diagnosis-admissions';
|
||||
export const POSTGRES_COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_MIGRATION_ID =
|
||||
'pg-9019-ai-copilot-failure-diagnosis-tool-unlocks';
|
||||
export const POSTGRES_MODEL_INVOCATION_SCHEMA = 'ql3_ai';
|
||||
export const LOCAL_MODEL_INVOCATION_MIGRATION_HISTORY_TABLE =
|
||||
'QingLong3AiSchemaMigrations';
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
POSTGRES_PLUGIN_PACKAGE_PROMPT_OUTPUT_KEY_ROTATION_MIGRATION_ID,
|
||||
POSTGRES_PLUGIN_PACKAGE_PROMPT_PRODUCT_AUTHORIZATION_MIGRATION_ID,
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_MIGRATION_ID,
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_MIGRATION_ID,
|
||||
POSTGRES_MODEL_INVOCATION_SCHEMA,
|
||||
POSTGRES_MODEL_INVOCATION_MIGRATION_HISTORY_TABLE,
|
||||
} from './identities';
|
||||
@@ -63,6 +64,7 @@ const POSTGRES_HISTORY_IDENTITY = Object.freeze({
|
||||
POSTGRES_MODEL_PROVIDER_CREDENTIAL_TEST_CONNECTION_MIGRATION_ID,
|
||||
POSTGRES_PLUGIN_PACKAGE_PROMPT_PRODUCT_AUTHORIZATION_MIGRATION_ID,
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_MIGRATION_ID,
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_MIGRATION_ID,
|
||||
]),
|
||||
streamId: POSTGRES_MODEL_INVOCATION_MIGRATION_STREAM_ID,
|
||||
dialect: 'postgresql' as const,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { PostgresQueryable } from '@qinglong/runtime-core';
|
||||
|
||||
import {
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_MIGRATION_ID,
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_MIGRATION_ID,
|
||||
POSTGRES_MODEL_INVOCATION_SCHEMA,
|
||||
} from '../identities';
|
||||
import { defineSqlMigration } from '../shared';
|
||||
@@ -9,6 +10,7 @@ import { defineSqlMigration } from '../shared';
|
||||
const ADMISSION_TABLE = 'copilot_failure_diagnosis_admissions';
|
||||
const SOURCE_SNAPSHOT_FUNCTION =
|
||||
'copilot_failure_diagnosis_admission_source_snapshot';
|
||||
const TOOL_UNLOCK_TABLE = 'copilot_failure_diagnosis_tool_unlocks';
|
||||
|
||||
const POSTGRES_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_TABLE_SQL = `
|
||||
CREATE TABLE "${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${ADMISSION_TABLE}" (
|
||||
@@ -215,6 +217,109 @@ const postgresCopilotFailureDiagnosisAdmissionMigration =
|
||||
(context, statement) => context.query(statement).then(() => undefined),
|
||||
);
|
||||
|
||||
const POSTGRES_COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_TABLE_SQL = `
|
||||
CREATE TABLE "${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${TOOL_UNLOCK_TABLE}" (
|
||||
request_id varchar(128) PRIMARY KEY,
|
||||
plan_digest char(64) NOT NULL UNIQUE,
|
||||
run_id varchar(36) NOT NULL,
|
||||
start_id varchar(36) NOT NULL UNIQUE,
|
||||
tool_step_run_id varchar(128) NOT NULL,
|
||||
tool_completion_digest char(64) NOT NULL UNIQUE,
|
||||
model_step_run_id varchar(128) NOT NULL UNIQUE,
|
||||
model_step_run_version integer NOT NULL,
|
||||
model_step_run_digest char(64) NOT NULL,
|
||||
model_mutation_id varchar(36) NOT NULL UNIQUE,
|
||||
model_mutation_digest char(64) NOT NULL UNIQUE,
|
||||
model_event_id varchar(36) NOT NULL UNIQUE,
|
||||
final_run_version integer NOT NULL,
|
||||
final_run_event_sequence integer NOT NULL,
|
||||
unlocked_at_ms bigint NOT NULL,
|
||||
receipt_digest char(64) NOT NULL UNIQUE,
|
||||
command_digest char(64) NOT NULL UNIQUE,
|
||||
receipt_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_ai_copilot_diagnosis_tool_unlock_admission_fk
|
||||
FOREIGN KEY (request_id)
|
||||
REFERENCES "${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${ADMISSION_TABLE}"
|
||||
(request_id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_ai_copilot_diagnosis_tool_unlock_completion_fk
|
||||
FOREIGN KEY (start_id)
|
||||
REFERENCES "ql3"."tool_execution_completions" (start_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_ai_copilot_diagnosis_tool_unlock_model_step_fk
|
||||
FOREIGN KEY (run_id, model_step_run_id)
|
||||
REFERENCES "ql3"."step_runs" (run_id, id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_ai_copilot_diagnosis_tool_unlock_mutation_fk
|
||||
FOREIGN KEY (model_mutation_id)
|
||||
REFERENCES "ql3"."step_run_mutations" (mutation_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_ai_copilot_diagnosis_tool_unlock_event_fk
|
||||
FOREIGN KEY (model_event_id)
|
||||
REFERENCES "ql3"."run_events" (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_ai_copilot_diagnosis_tool_unlock_identity_check CHECK (
|
||||
request_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,35}$' AND
|
||||
start_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,35}$' AND
|
||||
tool_step_run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
model_step_run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
model_mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,35}$' AND
|
||||
model_event_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,35}$'
|
||||
),
|
||||
CONSTRAINT ql3_ai_copilot_diagnosis_tool_unlock_version_check CHECK (
|
||||
model_step_run_version >= 2 AND
|
||||
final_run_version >= 1 AND
|
||||
final_run_event_sequence = final_run_version AND
|
||||
unlocked_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_ai_copilot_diagnosis_tool_unlock_digest_check CHECK (
|
||||
plan_digest ~ '^[0-9a-f]{64}$' AND
|
||||
tool_completion_digest ~ '^[0-9a-f]{64}$' AND
|
||||
model_step_run_digest ~ '^[0-9a-f]{64}$' AND
|
||||
model_mutation_digest ~ '^[0-9a-f]{64}$' AND
|
||||
receipt_digest ~ '^[0-9a-f]{64}$' AND
|
||||
command_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_ai_copilot_diagnosis_tool_unlock_json_check CHECK (
|
||||
jsonb_typeof(receipt_json) = 'object' AND
|
||||
octet_length(receipt_json::text) BETWEEN 2 AND 16384 AND
|
||||
receipt_json @> jsonb_build_object(
|
||||
'schema',
|
||||
'qinglong/copilot-failure-diagnosis-tool-unlock-receipt@v1',
|
||||
'requestId', request_id, 'planDigest', plan_digest,
|
||||
'runId', run_id, 'startId', start_id,
|
||||
'toolStepRunId', tool_step_run_id,
|
||||
'toolCompletionDigest', tool_completion_digest,
|
||||
'modelStepRunId', model_step_run_id,
|
||||
'modelStepRunVersion', model_step_run_version,
|
||||
'modelStepRunDigest', model_step_run_digest,
|
||||
'modelMutationId', model_mutation_id,
|
||||
'modelMutationDigest', model_mutation_digest,
|
||||
'modelEventId', model_event_id,
|
||||
'finalRunVersion', final_run_version,
|
||||
'finalRunEventSequence', final_run_event_sequence,
|
||||
'unlockedAtMs', unlocked_at_ms, 'receiptDigest', receipt_digest
|
||||
)
|
||||
)
|
||||
)`;
|
||||
|
||||
const postgresCopilotFailureDiagnosisToolUnlockMigration =
|
||||
defineSqlMigration<PostgresQueryable>(
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_MIGRATION_ID,
|
||||
[
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_TABLE_SQL,
|
||||
`CREATE UNIQUE INDEX ql3_ai_copilot_diagnosis_tool_unlock_fence_uidx
|
||||
ON "${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${TOOL_UNLOCK_TABLE}"
|
||||
(run_id, tool_step_run_id, model_step_run_id)`,
|
||||
`REVOKE ALL ON TABLE
|
||||
"${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${TOOL_UNLOCK_TABLE}"
|
||||
FROM PUBLIC`,
|
||||
`GRANT SELECT, INSERT ON TABLE
|
||||
"${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${TOOL_UNLOCK_TABLE}"
|
||||
TO ql3_runtime`,
|
||||
],
|
||||
(context, statement) => context.query(statement).then(() => undefined),
|
||||
);
|
||||
|
||||
export const postgresCopilotMigrations = Object.freeze([
|
||||
postgresCopilotFailureDiagnosisAdmissionMigration,
|
||||
postgresCopilotFailureDiagnosisToolUnlockMigration,
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user