mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 19:29:13 +08:00
310 lines
9.6 KiB
TypeScript
310 lines
9.6 KiB
TypeScript
import { RUN_STATUSES, type RunStatus } from '@qinglong/runtime-core';
|
|
import { normalizeProjectPolicySubject } from '@qinglong/runtime-core/project-policy';
|
|
import {
|
|
normalizeSecurityAuditRecord,
|
|
type SecurityAuditRecord,
|
|
} from '@qinglong/runtime-core/security-audit';
|
|
import type {
|
|
SecurityPolicyFence,
|
|
SecuritySubject,
|
|
} from '@qinglong/runtime-core/security';
|
|
import {
|
|
STEP_RUN_STATUSES,
|
|
type StepRunStatus,
|
|
} from '@qinglong/runtime-core/step-run';
|
|
|
|
import { PLUGIN_PACKAGE_PROMPT_FINAL_RUN_STATUSES } from './pluginPackagePromptExecution';
|
|
|
|
export const PLUGIN_PACKAGE_PROMPT_EXECUTION_INSPECTION_SCHEMA =
|
|
'qinglong/plugin-package-prompt-execution-inspection@v1' as const;
|
|
|
|
export interface AuthorizedPluginPackagePromptExecutionInspection {
|
|
readonly projectId: string;
|
|
readonly packageName: string;
|
|
readonly promptId: string;
|
|
readonly executionRequestId: string;
|
|
readonly actor: Readonly<SecuritySubject>;
|
|
readonly fence: Readonly<SecurityPolicyFence>;
|
|
readonly audit: Readonly<SecurityAuditRecord>;
|
|
}
|
|
|
|
export interface PluginPackagePromptExecutionInspection {
|
|
readonly invocationId: string;
|
|
readonly runId: string;
|
|
readonly stepRunId: string;
|
|
readonly runStatus:
|
|
| Extract<RunStatus, 'running'>
|
|
| (typeof PLUGIN_PACKAGE_PROMPT_FINAL_RUN_STATUSES)[number];
|
|
readonly runVersion: number;
|
|
readonly eventSequence: number;
|
|
readonly stepStatus: StepRunStatus;
|
|
readonly stepVersion: number;
|
|
readonly admittedAtMs: number;
|
|
readonly startedAtMs: number;
|
|
readonly finishedAtMs: number | null;
|
|
readonly finalizedAtMs: number | null;
|
|
}
|
|
|
|
export interface PluginPackagePromptExecutionInspectionResult {
|
|
readonly schema: typeof PLUGIN_PACKAGE_PROMPT_EXECUTION_INSPECTION_SCHEMA;
|
|
readonly found: boolean;
|
|
readonly projectId: string;
|
|
readonly packageName: string;
|
|
readonly promptId: string;
|
|
readonly executionRequestId: string;
|
|
readonly execution: Readonly<PluginPackagePromptExecutionInspection> | null;
|
|
}
|
|
|
|
export interface PluginPackagePromptExecutionInspectionRepository {
|
|
inspectAuthorized(
|
|
inspection: AuthorizedPluginPackagePromptExecutionInspection,
|
|
): Promise<Readonly<PluginPackagePromptExecutionInspectionResult>>;
|
|
}
|
|
|
|
export class InvalidPluginPackagePromptExecutionInspectionError extends TypeError {
|
|
readonly code = 'PLUGIN_PACKAGE_PROMPT_EXECUTION_INSPECTION_INVALID';
|
|
|
|
constructor() {
|
|
super('Plugin Package Prompt execution inspection is invalid');
|
|
this.name = 'InvalidPluginPackagePromptExecutionInspectionError';
|
|
}
|
|
}
|
|
|
|
export class PluginPackagePromptExecutionInspectionAuthorizationFenceConflictError extends Error {
|
|
readonly code =
|
|
'PLUGIN_PACKAGE_PROMPT_EXECUTION_INSPECTION_AUTHORIZATION_FENCE_CONFLICT';
|
|
|
|
constructor() {
|
|
super('Plugin Package Prompt execution inspection authorization changed');
|
|
this.name =
|
|
'PluginPackagePromptExecutionInspectionAuthorizationFenceConflictError';
|
|
}
|
|
}
|
|
|
|
export class PluginPackagePromptExecutionInspectionUnavailableError extends Error {
|
|
readonly code = 'PLUGIN_PACKAGE_PROMPT_EXECUTION_INSPECTION_UNAVAILABLE';
|
|
|
|
constructor(options?: ErrorOptions) {
|
|
super('Plugin Package Prompt execution inspection is unavailable', options);
|
|
this.name = 'PluginPackagePromptExecutionInspectionUnavailableError';
|
|
}
|
|
}
|
|
|
|
const IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/;
|
|
const RUN_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,35}$/;
|
|
const PACKAGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
const INSPECTABLE_RUN_STATUSES = Object.freeze([
|
|
'running',
|
|
...PLUGIN_PACKAGE_PROMPT_FINAL_RUN_STATUSES,
|
|
] as const);
|
|
|
|
function invalid(): never {
|
|
throw new InvalidPluginPackagePromptExecutionInspectionError();
|
|
}
|
|
|
|
function exactKeys(value: object, keys: readonly string[]): boolean {
|
|
const actual = Object.keys(value).sort();
|
|
const expected = [...keys].sort();
|
|
return (
|
|
actual.length === expected.length &&
|
|
actual.every((key, index) => key === expected[index])
|
|
);
|
|
}
|
|
|
|
function identity(value: unknown, pattern: RegExp = IDENTITY): string {
|
|
if (typeof value !== 'string' || !pattern.test(value)) return invalid();
|
|
return value;
|
|
}
|
|
|
|
function positiveInteger(value: unknown): number {
|
|
if (!Number.isSafeInteger(value) || (value as number) < 1) return invalid();
|
|
return value as number;
|
|
}
|
|
|
|
function nonNegativeInteger(value: unknown): number {
|
|
if (!Number.isSafeInteger(value) || (value as number) < 0) return invalid();
|
|
return value as number;
|
|
}
|
|
|
|
function nullableTimestamp(value: unknown): number | null {
|
|
return value === null ? null : nonNegativeInteger(value);
|
|
}
|
|
|
|
function sameSubject(
|
|
left: Readonly<SecuritySubject>,
|
|
right: Readonly<SecuritySubject>,
|
|
): boolean {
|
|
return left.type === right.type && left.id === right.id;
|
|
}
|
|
|
|
export function normalizeAuthorizedPluginPackagePromptExecutionInspection(
|
|
value: AuthorizedPluginPackagePromptExecutionInspection,
|
|
): Readonly<AuthorizedPluginPackagePromptExecutionInspection> {
|
|
if (
|
|
!value ||
|
|
typeof value !== 'object' ||
|
|
Array.isArray(value) ||
|
|
!exactKeys(value, [
|
|
'actor',
|
|
'audit',
|
|
'executionRequestId',
|
|
'fence',
|
|
'packageName',
|
|
'projectId',
|
|
'promptId',
|
|
])
|
|
) {
|
|
return invalid();
|
|
}
|
|
try {
|
|
const projectId = identity(value.projectId);
|
|
const packageName = identity(value.packageName, PACKAGE_NAME);
|
|
const promptId = identity(value.promptId);
|
|
const executionRequestId = identity(value.executionRequestId);
|
|
const actor = normalizeProjectPolicySubject(value.actor);
|
|
const fence = value.fence;
|
|
if (
|
|
!fence ||
|
|
typeof fence !== 'object' ||
|
|
Array.isArray(fence) ||
|
|
!exactKeys(fence, ['bindingVersion', 'projectVersion']) ||
|
|
!Number.isSafeInteger(fence.projectVersion) ||
|
|
fence.projectVersion < 1 ||
|
|
!Number.isSafeInteger(fence.bindingVersion) ||
|
|
(fence.bindingVersion as number) < 1
|
|
) {
|
|
return invalid();
|
|
}
|
|
const audit = normalizeSecurityAuditRecord(value.audit);
|
|
if (
|
|
audit.operationId !== 'prompt.execution.read' ||
|
|
audit.projectId !== projectId ||
|
|
audit.outcome !== 'allowed' ||
|
|
!audit.subject ||
|
|
!sameSubject(audit.subject, actor) ||
|
|
audit.authenticationId === null ||
|
|
!audit.fence ||
|
|
audit.fence.projectVersion !== fence.projectVersion ||
|
|
audit.fence.bindingVersion !== fence.bindingVersion
|
|
) {
|
|
return invalid();
|
|
}
|
|
return Object.freeze({
|
|
projectId,
|
|
packageName,
|
|
promptId,
|
|
executionRequestId,
|
|
actor,
|
|
fence: Object.freeze({
|
|
projectVersion: fence.projectVersion,
|
|
bindingVersion: fence.bindingVersion as number,
|
|
}),
|
|
audit,
|
|
});
|
|
} catch {
|
|
return invalid();
|
|
}
|
|
}
|
|
|
|
export function normalizePluginPackagePromptExecutionInspectionResult(
|
|
value: PluginPackagePromptExecutionInspectionResult,
|
|
): Readonly<PluginPackagePromptExecutionInspectionResult> {
|
|
if (
|
|
!value ||
|
|
typeof value !== 'object' ||
|
|
Array.isArray(value) ||
|
|
!exactKeys(value, [
|
|
'execution',
|
|
'executionRequestId',
|
|
'found',
|
|
'packageName',
|
|
'projectId',
|
|
'promptId',
|
|
'schema',
|
|
]) ||
|
|
value.schema !== PLUGIN_PACKAGE_PROMPT_EXECUTION_INSPECTION_SCHEMA ||
|
|
typeof value.found !== 'boolean'
|
|
) {
|
|
return invalid();
|
|
}
|
|
const projectId = identity(value.projectId);
|
|
const packageName = identity(value.packageName, PACKAGE_NAME);
|
|
const promptId = identity(value.promptId);
|
|
const executionRequestId = identity(value.executionRequestId);
|
|
if (!value.found) {
|
|
if (value.execution !== null) return invalid();
|
|
return Object.freeze({
|
|
schema: PLUGIN_PACKAGE_PROMPT_EXECUTION_INSPECTION_SCHEMA,
|
|
found: false,
|
|
projectId,
|
|
packageName,
|
|
promptId,
|
|
executionRequestId,
|
|
execution: null,
|
|
});
|
|
}
|
|
const execution = value.execution;
|
|
if (
|
|
!execution ||
|
|
typeof execution !== 'object' ||
|
|
Array.isArray(execution) ||
|
|
!exactKeys(execution, [
|
|
'admittedAtMs',
|
|
'eventSequence',
|
|
'finalizedAtMs',
|
|
'finishedAtMs',
|
|
'invocationId',
|
|
'runId',
|
|
'runStatus',
|
|
'runVersion',
|
|
'startedAtMs',
|
|
'stepRunId',
|
|
'stepStatus',
|
|
'stepVersion',
|
|
]) ||
|
|
!RUN_STATUSES.includes(execution.runStatus) ||
|
|
!INSPECTABLE_RUN_STATUSES.includes(
|
|
execution.runStatus as (typeof INSPECTABLE_RUN_STATUSES)[number],
|
|
) ||
|
|
!STEP_RUN_STATUSES.includes(execution.stepStatus)
|
|
) {
|
|
return invalid();
|
|
}
|
|
const admittedAtMs = nonNegativeInteger(execution.admittedAtMs);
|
|
const startedAtMs = nonNegativeInteger(execution.startedAtMs);
|
|
const finishedAtMs = nullableTimestamp(execution.finishedAtMs);
|
|
const finalizedAtMs = nullableTimestamp(execution.finalizedAtMs);
|
|
const terminal = execution.runStatus !== 'running';
|
|
if (
|
|
admittedAtMs !== startedAtMs ||
|
|
terminal !== (finishedAtMs !== null) ||
|
|
terminal !== (finalizedAtMs !== null) ||
|
|
(terminal && finishedAtMs !== finalizedAtMs) ||
|
|
(terminal && execution.stepStatus !== execution.runStatus)
|
|
) {
|
|
return invalid();
|
|
}
|
|
return Object.freeze({
|
|
schema: PLUGIN_PACKAGE_PROMPT_EXECUTION_INSPECTION_SCHEMA,
|
|
found: true,
|
|
projectId,
|
|
packageName,
|
|
promptId,
|
|
executionRequestId,
|
|
execution: Object.freeze({
|
|
invocationId: identity(execution.invocationId),
|
|
runId: identity(execution.runId, RUN_ID),
|
|
stepRunId: identity(execution.stepRunId),
|
|
runStatus: execution.runStatus,
|
|
runVersion: positiveInteger(execution.runVersion),
|
|
eventSequence: nonNegativeInteger(execution.eventSequence),
|
|
stepStatus: execution.stepStatus,
|
|
stepVersion: positiveInteger(execution.stepVersion),
|
|
admittedAtMs,
|
|
startedAtMs,
|
|
finishedAtMs,
|
|
finalizedAtMs,
|
|
}),
|
|
});
|
|
}
|