mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 09:58:46 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
+88
@@ -0,0 +1,88 @@
|
||||
import type { ClusterControlAdmissionResponse } from '../../transport/httpSurface';
|
||||
import type {
|
||||
ClusterControlAuthorizedOperationRequest,
|
||||
ClusterControlRouteDefinition,
|
||||
ClusterControlRouteParameters,
|
||||
} from '../../transport/routeRegistry';
|
||||
|
||||
export const CLUSTER_PLUGIN_PACKAGE_PROMPT_CATALOG_RESPONSE_SCHEMA =
|
||||
'qinglong/plugin-package-prompt-catalog@v1' as const;
|
||||
|
||||
export interface ClusterPluginPackagePromptCatalogCapability {
|
||||
inspect(
|
||||
projectId: string,
|
||||
packageName: string,
|
||||
): Promise<
|
||||
Readonly<{
|
||||
schema: typeof CLUSTER_PLUGIN_PACKAGE_PROMPT_CATALOG_RESPONSE_SCHEMA;
|
||||
projectId: string;
|
||||
packageName: string;
|
||||
found: boolean;
|
||||
publicationState: 'active' | 'withdrawn' | 'absent' | null;
|
||||
prompts: readonly Readonly<{
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
parameters: readonly Readonly<{
|
||||
name: string;
|
||||
description: string | null;
|
||||
required: boolean;
|
||||
}>[];
|
||||
}>[];
|
||||
}>
|
||||
>;
|
||||
}
|
||||
|
||||
const PACKAGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
||||
|
||||
function response(
|
||||
statusCode: number,
|
||||
body: Readonly<Record<string, unknown>>,
|
||||
): ClusterControlAdmissionResponse {
|
||||
return Object.freeze({ statusCode, body: Object.freeze(body) });
|
||||
}
|
||||
|
||||
export function createClusterControlPluginPackagePromptCatalogRoute(
|
||||
capability: ClusterPluginPackagePromptCatalogCapability,
|
||||
): Readonly<ClusterControlRouteDefinition> {
|
||||
if (!capability || typeof capability.inspect !== 'function') {
|
||||
throw new TypeError('Cluster-control Prompt catalog capability is invalid');
|
||||
}
|
||||
return Object.freeze({
|
||||
method: 'GET' as const,
|
||||
path: '/api/v3/projects/{projectId}/packages/{packageName}/prompts',
|
||||
operationId: 'prompt.read',
|
||||
permission: 'model.invoke',
|
||||
projectParameter: 'projectId' as const,
|
||||
async handle(
|
||||
authorized: ClusterControlAuthorizedOperationRequest,
|
||||
parameters: ClusterControlRouteParameters,
|
||||
) {
|
||||
if (
|
||||
authorized.request.body !== null ||
|
||||
authorized.projectId === null ||
|
||||
typeof parameters.packageName !== 'string' ||
|
||||
!PACKAGE_NAME.test(parameters.packageName)
|
||||
) {
|
||||
return response(400, { code: 'invalid_prompt_catalog_request' });
|
||||
}
|
||||
try {
|
||||
const result = await capability.inspect(
|
||||
authorized.projectId,
|
||||
parameters.packageName,
|
||||
);
|
||||
if (
|
||||
result.schema !==
|
||||
CLUSTER_PLUGIN_PACKAGE_PROMPT_CATALOG_RESPONSE_SCHEMA ||
|
||||
result.projectId !== authorized.projectId ||
|
||||
result.packageName !== parameters.packageName
|
||||
) {
|
||||
return response(503, { code: 'prompt_catalog_unavailable' });
|
||||
}
|
||||
return response(200, result);
|
||||
} catch {
|
||||
return response(503, { code: 'prompt_catalog_unavailable' });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import {
|
||||
normalizeSecurityAuditRecord,
|
||||
type SecurityAuditRecord,
|
||||
} from '@qinglong/runtime-core/security-audit';
|
||||
import type {
|
||||
SecurityPolicyFence,
|
||||
SecuritySubject,
|
||||
} from '@qinglong/runtime-core/security';
|
||||
|
||||
import type { ClusterControlAdmissionResponse } from '../../transport/httpSurface';
|
||||
import type {
|
||||
ClusterControlAuthorizedOperationRequest,
|
||||
ClusterControlRouteDefinition,
|
||||
ClusterControlRouteParameters,
|
||||
} from '../../transport/routeRegistry';
|
||||
|
||||
export const CLUSTER_CONTROL_PLUGIN_PACKAGE_PROMPT_EXECUTION_INSPECTION_ROUTE =
|
||||
Object.freeze({
|
||||
method: 'GET' as const,
|
||||
path: '/api/v3/projects/{projectId}/packages/{packageName}/prompts/{promptId}/executions/{executionRequestId}',
|
||||
operationId: 'prompt.execution.read',
|
||||
permission: 'run.read',
|
||||
projectParameter: 'projectId',
|
||||
});
|
||||
|
||||
export interface ClusterPluginPackagePromptExecutionInspectionRouteOptions {
|
||||
readonly now?: () => number;
|
||||
readonly createEventId?: () => string;
|
||||
}
|
||||
|
||||
export interface ClusterPluginPackagePromptExecutionInspectionCapability {
|
||||
inspectAuthorized(input: Readonly<{
|
||||
projectId: string;
|
||||
packageName: string;
|
||||
promptId: string;
|
||||
executionRequestId: string;
|
||||
actor: Readonly<SecuritySubject>;
|
||||
fence: Readonly<SecurityPolicyFence>;
|
||||
audit: Readonly<SecurityAuditRecord>;
|
||||
}>): Promise<Readonly<{ found: boolean }>>;
|
||||
}
|
||||
|
||||
const PACKAGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
||||
const PROMPT_ID = /^[a-z][a-z0-9-]{0,62}$/;
|
||||
const IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/;
|
||||
const UUID_V4 =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
||||
|
||||
function response(
|
||||
statusCode: number,
|
||||
body: Readonly<Record<string, unknown>>,
|
||||
): ClusterControlAdmissionResponse {
|
||||
return Object.freeze({ statusCode, body: Object.freeze(body) });
|
||||
}
|
||||
|
||||
function errorCode(error: unknown): string | null {
|
||||
return error &&
|
||||
typeof error === 'object' &&
|
||||
'code' in error &&
|
||||
typeof error.code === 'string'
|
||||
? error.code
|
||||
: null;
|
||||
}
|
||||
|
||||
/** Exact, content-free recovery read keyed by the caller-known requestId. */
|
||||
export function createClusterControlPluginPackagePromptExecutionInspectionRoute(
|
||||
capability: ClusterPluginPackagePromptExecutionInspectionCapability,
|
||||
options: ClusterPluginPackagePromptExecutionInspectionRouteOptions = {},
|
||||
): Readonly<ClusterControlRouteDefinition> {
|
||||
if (
|
||||
!capability ||
|
||||
typeof capability.inspectAuthorized !== 'function' ||
|
||||
(options.now !== undefined && typeof options.now !== 'function') ||
|
||||
(options.createEventId !== undefined &&
|
||||
typeof options.createEventId !== 'function')
|
||||
) {
|
||||
throw new TypeError(
|
||||
'Cluster-control Prompt execution inspection capability is invalid',
|
||||
);
|
||||
}
|
||||
const now = options.now ?? Date.now;
|
||||
const createEventId = options.createEventId ?? randomUUID;
|
||||
return Object.freeze({
|
||||
...CLUSTER_CONTROL_PLUGIN_PACKAGE_PROMPT_EXECUTION_INSPECTION_ROUTE,
|
||||
async handle(
|
||||
authorized: ClusterControlAuthorizedOperationRequest,
|
||||
parameters: ClusterControlRouteParameters,
|
||||
) {
|
||||
const observedAtMs = now();
|
||||
const auditEventId = createEventId();
|
||||
if (authorized.request.body !== null) {
|
||||
return response(400, { code: 'invalid_request_body' });
|
||||
}
|
||||
if (
|
||||
authorized.projectId === null ||
|
||||
typeof parameters.packageName !== 'string' ||
|
||||
!PACKAGE_NAME.test(parameters.packageName) ||
|
||||
typeof parameters.promptId !== 'string' ||
|
||||
!PROMPT_ID.test(parameters.promptId) ||
|
||||
typeof parameters.executionRequestId !== 'string' ||
|
||||
!IDENTITY.test(parameters.executionRequestId) ||
|
||||
!authorized.policyFence ||
|
||||
authorized.policyFence.bindingVersion === null ||
|
||||
!Number.isSafeInteger(observedAtMs) ||
|
||||
observedAtMs < 0 ||
|
||||
typeof auditEventId !== 'string' ||
|
||||
!UUID_V4.test(auditEventId)
|
||||
) {
|
||||
return response(503, { code: 'prompt_execution_inspection_unavailable' });
|
||||
}
|
||||
try {
|
||||
const result = await capability.inspectAuthorized({
|
||||
projectId: authorized.projectId,
|
||||
packageName: parameters.packageName,
|
||||
promptId: parameters.promptId,
|
||||
executionRequestId: parameters.executionRequestId,
|
||||
actor: authorized.principal.subject,
|
||||
fence: {
|
||||
projectVersion: authorized.policyFence.projectVersion,
|
||||
bindingVersion: authorized.policyFence.bindingVersion,
|
||||
},
|
||||
audit: normalizeSecurityAuditRecord({
|
||||
eventId: auditEventId,
|
||||
requestId: authorized.request.requestId,
|
||||
operationId: 'prompt.execution.read',
|
||||
projectId: authorized.projectId,
|
||||
subject: authorized.principal.subject,
|
||||
authenticationId: authorized.principal.authenticationId,
|
||||
outcome: 'allowed',
|
||||
reasons: ['project_policy_allowed'],
|
||||
fence: authorized.policyFence,
|
||||
occurredAtMs: observedAtMs,
|
||||
}),
|
||||
});
|
||||
return result.found
|
||||
? response(200, { ...result })
|
||||
: response(404, { code: 'prompt_execution_not_found' });
|
||||
} catch (error) {
|
||||
return errorCode(error) ===
|
||||
'PLUGIN_PACKAGE_PROMPT_EXECUTION_INSPECTION_AUTHORIZATION_FENCE_CONFLICT'
|
||||
? response(409, { code: 'authorization_fence_conflict' })
|
||||
: response(503, {
|
||||
code: 'prompt_execution_inspection_unavailable',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
// Plugin Package Prompt owns request-keyed durable output recovery.
|
||||
import type { SecurityPrincipal } from '@qinglong/runtime-core/security';
|
||||
|
||||
import type { ClusterControlAdmissionResponse } from '../../transport/httpSurface';
|
||||
import type {
|
||||
ClusterControlAuthorizedOperationRequest,
|
||||
ClusterControlRouteDefinition,
|
||||
ClusterControlRouteParameters,
|
||||
} from '../../transport/routeRegistry';
|
||||
|
||||
export const CLUSTER_PLUGIN_PACKAGE_PROMPT_EXECUTION_OUTPUT_READ_RESPONSE_SCHEMA =
|
||||
'qinglong/cluster-plugin-package-prompt-execution-output-read-response@v1' as const;
|
||||
export const CLUSTER_CONTROL_PLUGIN_PACKAGE_PROMPT_EXECUTION_OUTPUT_READ_ROUTE =
|
||||
Object.freeze({
|
||||
method: 'GET' as const,
|
||||
path: '/api/v3/projects/{projectId}/packages/{packageName}/prompts/{promptId}/executions/{executionRequestId}/output',
|
||||
operationId: 'prompt.execution.output.read',
|
||||
permission: 'artifact.read',
|
||||
projectParameter: 'projectId',
|
||||
});
|
||||
|
||||
export interface ClusterPluginPackagePromptExecutionOutputReadCapability {
|
||||
read(command: Readonly<{
|
||||
principal: Readonly<SecurityPrincipal>;
|
||||
projectId: string;
|
||||
packageName: string;
|
||||
promptId: string;
|
||||
executionRequestId: string;
|
||||
}>): Promise<unknown>;
|
||||
}
|
||||
|
||||
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 PROMPT_ID = /^[a-z][a-z0-9-]{0,62}$/;
|
||||
const MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/;
|
||||
const DIGEST = /^[0-9a-f]{64}$/;
|
||||
const KEY_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
||||
const FINISH_REASONS = new Set([
|
||||
'stop',
|
||||
'length',
|
||||
'content_filter',
|
||||
'tool_call',
|
||||
'unknown',
|
||||
]);
|
||||
|
||||
function response(
|
||||
statusCode: number,
|
||||
body: Readonly<Record<string, unknown>>,
|
||||
): ClusterControlAdmissionResponse {
|
||||
return Object.freeze({ statusCode, body: Object.freeze(body) });
|
||||
}
|
||||
|
||||
function exactRecord(
|
||||
value: unknown,
|
||||
required: readonly string[],
|
||||
optional: readonly string[] = [],
|
||||
): Record<string, unknown> | null {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
||||
const record = value as Record<string, unknown>;
|
||||
const keys = Object.keys(record).sort();
|
||||
const expected = [
|
||||
...required,
|
||||
...optional.filter((key) => key in record),
|
||||
].sort();
|
||||
return keys.length === expected.length &&
|
||||
keys.every((key, index) => key === expected[index])
|
||||
? record
|
||||
: null;
|
||||
}
|
||||
|
||||
function nonNegativeInteger(value: unknown): value is number {
|
||||
return Number.isSafeInteger(value) && (value as number) >= 0;
|
||||
}
|
||||
|
||||
function exactTarget(
|
||||
value: Record<string, unknown>,
|
||||
expected: Readonly<{
|
||||
projectId: string;
|
||||
packageName: string;
|
||||
promptId: string;
|
||||
executionRequestId: string;
|
||||
}>,
|
||||
): boolean {
|
||||
return (
|
||||
value.projectId === expected.projectId &&
|
||||
value.packageName === expected.packageName &&
|
||||
value.promptId === expected.promptId &&
|
||||
value.executionRequestId === expected.executionRequestId
|
||||
);
|
||||
}
|
||||
|
||||
function availableView(
|
||||
value: unknown,
|
||||
expected: Readonly<{
|
||||
projectId: string;
|
||||
packageName: string;
|
||||
promptId: string;
|
||||
executionRequestId: string;
|
||||
}>,
|
||||
): Readonly<Record<string, unknown>> | null {
|
||||
const envelope = exactRecord(value, [
|
||||
'executionRequestId',
|
||||
'packageName',
|
||||
'projectId',
|
||||
'promptId',
|
||||
'reference',
|
||||
'result',
|
||||
'schema',
|
||||
'status',
|
||||
]);
|
||||
if (
|
||||
!envelope ||
|
||||
envelope.schema !==
|
||||
'qinglong/plugin-package-prompt-execution-output-read-result@v1' ||
|
||||
envelope.status !== 'available' ||
|
||||
!exactTarget(envelope, expected)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const reference = exactRecord(envelope.reference, [
|
||||
'algorithm',
|
||||
'artifactDigest',
|
||||
'artifactId',
|
||||
'contentDigest',
|
||||
'invocationId',
|
||||
'keyId',
|
||||
'outputBytes',
|
||||
'projectId',
|
||||
'retentionEligibleAtMs',
|
||||
'retentionPolicyDigest',
|
||||
'runId',
|
||||
'schema',
|
||||
'stepRunId',
|
||||
]);
|
||||
const result = exactRecord(envelope.result, [
|
||||
'finishReason',
|
||||
'model',
|
||||
'provider',
|
||||
'text',
|
||||
'usage',
|
||||
]);
|
||||
const usage = result
|
||||
? exactRecord(
|
||||
result.usage,
|
||||
['inputTokens', 'outputTokens', 'totalTokens'],
|
||||
['costMicros'],
|
||||
)
|
||||
: null;
|
||||
if (
|
||||
!reference ||
|
||||
!result ||
|
||||
!usage ||
|
||||
reference.schema !==
|
||||
'qinglong/plugin-package-prompt-output-artifact-reference@v1' ||
|
||||
reference.algorithm !== 'aes-256-gcm' ||
|
||||
reference.projectId !== expected.projectId ||
|
||||
typeof reference.runId !== 'string' ||
|
||||
!RUN_ID.test(reference.runId) ||
|
||||
typeof reference.artifactId !== 'string' ||
|
||||
!IDENTITY.test(reference.artifactId) ||
|
||||
typeof reference.artifactDigest !== 'string' ||
|
||||
!DIGEST.test(reference.artifactDigest) ||
|
||||
typeof reference.stepRunId !== 'string' ||
|
||||
!IDENTITY.test(reference.stepRunId) ||
|
||||
typeof reference.invocationId !== 'string' ||
|
||||
!IDENTITY.test(reference.invocationId) ||
|
||||
typeof reference.contentDigest !== 'string' ||
|
||||
!DIGEST.test(reference.contentDigest) ||
|
||||
!nonNegativeInteger(reference.outputBytes) ||
|
||||
reference.outputBytes > 1024 * 1024 ||
|
||||
typeof reference.retentionPolicyDigest !== 'string' ||
|
||||
!DIGEST.test(reference.retentionPolicyDigest) ||
|
||||
!nonNegativeInteger(reference.retentionEligibleAtMs) ||
|
||||
typeof reference.keyId !== 'string' ||
|
||||
!KEY_ID.test(reference.keyId) ||
|
||||
typeof result.provider !== 'string' ||
|
||||
!MODEL_ID.test(result.provider) ||
|
||||
typeof result.model !== 'string' ||
|
||||
!MODEL_ID.test(result.model) ||
|
||||
typeof result.text !== 'string' ||
|
||||
Buffer.byteLength(result.text, 'utf8') > 1024 * 1024 ||
|
||||
!FINISH_REASONS.has(result.finishReason as string) ||
|
||||
!nonNegativeInteger(usage.inputTokens) ||
|
||||
!nonNegativeInteger(usage.outputTokens) ||
|
||||
!nonNegativeInteger(usage.totalTokens) ||
|
||||
usage.totalTokens !== usage.inputTokens + usage.outputTokens ||
|
||||
(usage.costMicros !== undefined && !nonNegativeInteger(usage.costMicros))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return Object.freeze({
|
||||
schema:
|
||||
CLUSTER_PLUGIN_PACKAGE_PROMPT_EXECUTION_OUTPUT_READ_RESPONSE_SCHEMA,
|
||||
status: 'available',
|
||||
...expected,
|
||||
reference: Object.freeze({ ...reference }),
|
||||
result: Object.freeze({
|
||||
provider: result.provider,
|
||||
model: result.model,
|
||||
text: result.text,
|
||||
finishReason: result.finishReason,
|
||||
usage: Object.freeze({ ...usage }),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export function createClusterControlPluginPackagePromptExecutionOutputReadRoute(
|
||||
capability: ClusterPluginPackagePromptExecutionOutputReadCapability,
|
||||
): Readonly<ClusterControlRouteDefinition> {
|
||||
if (!capability || typeof capability.read !== 'function') {
|
||||
throw new TypeError(
|
||||
'Cluster-control Prompt execution output read capability is invalid',
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
...CLUSTER_CONTROL_PLUGIN_PACKAGE_PROMPT_EXECUTION_OUTPUT_READ_ROUTE,
|
||||
async handle(
|
||||
authorized: ClusterControlAuthorizedOperationRequest,
|
||||
parameters: ClusterControlRouteParameters,
|
||||
) {
|
||||
if (
|
||||
authorized.request.body !== null ||
|
||||
authorized.projectId === null ||
|
||||
typeof parameters.packageName !== 'string' ||
|
||||
!PACKAGE_NAME.test(parameters.packageName) ||
|
||||
typeof parameters.promptId !== 'string' ||
|
||||
!PROMPT_ID.test(parameters.promptId) ||
|
||||
typeof parameters.executionRequestId !== 'string' ||
|
||||
!IDENTITY.test(parameters.executionRequestId)
|
||||
) {
|
||||
return response(400, {
|
||||
code: 'invalid_prompt_execution_output_read_request',
|
||||
});
|
||||
}
|
||||
const expected = Object.freeze({
|
||||
projectId: authorized.projectId,
|
||||
packageName: parameters.packageName,
|
||||
promptId: parameters.promptId,
|
||||
executionRequestId: parameters.executionRequestId,
|
||||
});
|
||||
try {
|
||||
const result = await capability.read({
|
||||
principal: authorized.principal,
|
||||
...expected,
|
||||
});
|
||||
const notFound = exactRecord(result, [
|
||||
'executionRequestId',
|
||||
'packageName',
|
||||
'projectId',
|
||||
'promptId',
|
||||
'schema',
|
||||
'status',
|
||||
]);
|
||||
if (
|
||||
notFound &&
|
||||
notFound.schema ===
|
||||
'qinglong/plugin-package-prompt-execution-output-read-result@v1' &&
|
||||
notFound.status === 'not_found' &&
|
||||
exactTarget(notFound, expected)
|
||||
) {
|
||||
return response(404, { code: 'prompt_execution_output_not_found' });
|
||||
}
|
||||
const view = availableView(result, expected);
|
||||
return view
|
||||
? response(200, view)
|
||||
: response(503, {
|
||||
code: 'prompt_execution_output_read_unavailable',
|
||||
});
|
||||
} catch {
|
||||
return response(503, {
|
||||
code: 'prompt_execution_output_read_unavailable',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
+400
@@ -0,0 +1,400 @@
|
||||
// Plugin Package Prompt owns bounded, Policy-fenced model execution admission.
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { SecurityPrincipal } from '@qinglong/runtime-core/security';
|
||||
import type { ClusterControlAdmissionResponse } from '../../transport/httpSurface';
|
||||
import type {
|
||||
ClusterControlAuthorizedOperationRequest,
|
||||
ClusterControlRouteDefinition,
|
||||
ClusterControlRouteParameters,
|
||||
} from '../../transport/routeRegistry';
|
||||
|
||||
export const CLUSTER_PLUGIN_PACKAGE_PROMPT_EXECUTION_REQUEST_SCHEMA =
|
||||
'qinglong/cluster-plugin-package-prompt-execution-request@v2' as const;
|
||||
export const CLUSTER_PLUGIN_PACKAGE_PROMPT_EXECUTION_RESPONSE_SCHEMA =
|
||||
'qinglong/cluster-plugin-package-prompt-execution-response@v2' as const;
|
||||
export const CLUSTER_CONTROL_PLUGIN_PACKAGE_PROMPT_EXECUTION_ROUTE =
|
||||
Object.freeze({
|
||||
method: 'POST' as const,
|
||||
path: '/api/v3/projects/{projectId}/packages/{packageName}/prompts/{promptId}/executions',
|
||||
operationId: 'prompt.execute',
|
||||
permission: 'model.invoke',
|
||||
projectParameter: 'projectId',
|
||||
});
|
||||
|
||||
export const CLUSTER_PLUGIN_PACKAGE_PROMPT_EXECUTION_LIMITS = Object.freeze({
|
||||
maxParameters: 64,
|
||||
maxParameterValueBytes: 64 * 1024,
|
||||
maxOutputTokens: 32_768,
|
||||
maxExecutionMs: 120_000,
|
||||
minOutputRetentionMs: 60 * 60_000,
|
||||
maxOutputRetentionMs: 365 * 24 * 60 * 60_000,
|
||||
});
|
||||
|
||||
export type ClusterPluginPackagePromptOutputIntent =
|
||||
| Readonly<{ mode: 'live_only' }>
|
||||
| Readonly<{
|
||||
mode: 'durable_artifact';
|
||||
retentionPolicy: Readonly<{
|
||||
revision: string;
|
||||
retentionMs: number;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
export interface ClusterPluginPackagePromptExecutionCommand {
|
||||
readonly projectId: string;
|
||||
readonly packageName: string;
|
||||
readonly promptId: string;
|
||||
readonly requestId: string;
|
||||
readonly traceId: string;
|
||||
readonly auditEventId: string;
|
||||
readonly principal: Readonly<SecurityPrincipal>;
|
||||
readonly policyFence: Readonly<{
|
||||
readonly projectVersion: number;
|
||||
readonly bindingVersion: number;
|
||||
}>;
|
||||
readonly parameters: Readonly<Record<string, string>>;
|
||||
readonly provider: string;
|
||||
readonly model: string;
|
||||
readonly maxOutputTokens: number;
|
||||
readonly temperature?: number;
|
||||
readonly deadlineAtMs: number;
|
||||
readonly plannedAtMs: number;
|
||||
readonly output?: Readonly<ClusterPluginPackagePromptOutputIntent>;
|
||||
readonly signal: AbortSignal;
|
||||
}
|
||||
|
||||
export interface ClusterPluginPackagePromptExecutionCapability {
|
||||
execute(command: Readonly<ClusterPluginPackagePromptExecutionCommand>): Promise<
|
||||
Readonly<{
|
||||
readonly status: 'executed' | 'resumed' | 'existing';
|
||||
readonly admission: Readonly<{
|
||||
readonly requestId: string;
|
||||
readonly invocationId: string;
|
||||
readonly runId: string;
|
||||
readonly stepRunId: string;
|
||||
}>;
|
||||
readonly finalization: Readonly<{ readonly runStatus: string }>;
|
||||
readonly result: unknown | null;
|
||||
readonly outputArtifact?: unknown;
|
||||
}>
|
||||
>;
|
||||
}
|
||||
|
||||
export interface ClusterPluginPackagePromptExecutionRouteOptions {
|
||||
readonly maxExecutionMs?: number;
|
||||
readonly now?: () => number;
|
||||
readonly createEventId?: () => string;
|
||||
}
|
||||
|
||||
class InvalidPromptExecutionRequestError extends TypeError {}
|
||||
|
||||
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
const PACKAGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
||||
const UUID_V4 =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
||||
|
||||
function invalid(): never {
|
||||
throw new InvalidPromptExecutionRequestError();
|
||||
}
|
||||
|
||||
function dataRecord(value: unknown): Record<string, unknown> {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
(Object.getPrototypeOf(value) !== Object.prototype &&
|
||||
Object.getPrototypeOf(value) !== null)
|
||||
) {
|
||||
return invalid();
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function exactKeys(
|
||||
value: Record<string, unknown>,
|
||||
required: readonly string[],
|
||||
optional: readonly string[] = [],
|
||||
): void {
|
||||
const allowed = new Set([...required, ...optional]);
|
||||
const keys = Object.keys(value);
|
||||
if (
|
||||
required.some((key) => !keys.includes(key)) ||
|
||||
keys.some((key) => !allowed.has(key))
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
}
|
||||
|
||||
function identifier(value: unknown): string {
|
||||
if (typeof value !== 'string' || !IDENTIFIER.test(value)) return invalid();
|
||||
return value;
|
||||
}
|
||||
|
||||
function positiveInteger(value: unknown, maximum: number): number {
|
||||
if (
|
||||
typeof value !== 'number' ||
|
||||
!Number.isSafeInteger(value) ||
|
||||
value < 1 ||
|
||||
value > maximum
|
||||
) {
|
||||
return invalid();
|
||||
}
|
||||
return value as number;
|
||||
}
|
||||
|
||||
function parameters(value: unknown): Readonly<Record<string, string>> {
|
||||
const record = dataRecord(value);
|
||||
const names = Object.keys(record).sort();
|
||||
if (
|
||||
names.length > CLUSTER_PLUGIN_PACKAGE_PROMPT_EXECUTION_LIMITS.maxParameters
|
||||
) {
|
||||
return invalid();
|
||||
}
|
||||
const normalized = Object.create(null) as Record<string, string>;
|
||||
for (const name of names) {
|
||||
const parameter = record[name];
|
||||
if (
|
||||
!/^[A-Za-z][A-Za-z0-9_.-]{0,63}$/.test(name) ||
|
||||
typeof parameter !== 'string' ||
|
||||
Buffer.byteLength(parameter, 'utf8') >
|
||||
CLUSTER_PLUGIN_PACKAGE_PROMPT_EXECUTION_LIMITS.maxParameterValueBytes
|
||||
) {
|
||||
return invalid();
|
||||
}
|
||||
normalized[name] = parameter;
|
||||
}
|
||||
return Object.freeze(normalized);
|
||||
}
|
||||
|
||||
function outputIntent(
|
||||
value: unknown,
|
||||
): Readonly<ClusterPluginPackagePromptOutputIntent> {
|
||||
const output = dataRecord(value);
|
||||
if (output.mode === 'live_only') {
|
||||
exactKeys(output, ['mode']);
|
||||
return Object.freeze({ mode: 'live_only' as const });
|
||||
}
|
||||
if (output.mode !== 'durable_artifact') return invalid();
|
||||
exactKeys(output, ['mode', 'retentionPolicy']);
|
||||
const retention = dataRecord(output.retentionPolicy);
|
||||
exactKeys(retention, ['retentionMs', 'revision']);
|
||||
if (
|
||||
typeof retention.revision !== 'string' ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/.test(retention.revision) ||
|
||||
!Number.isSafeInteger(retention.retentionMs) ||
|
||||
(retention.retentionMs as number) <
|
||||
CLUSTER_PLUGIN_PACKAGE_PROMPT_EXECUTION_LIMITS.minOutputRetentionMs ||
|
||||
(retention.retentionMs as number) >
|
||||
CLUSTER_PLUGIN_PACKAGE_PROMPT_EXECUTION_LIMITS.maxOutputRetentionMs
|
||||
) {
|
||||
return invalid();
|
||||
}
|
||||
return Object.freeze({
|
||||
mode: 'durable_artifact' as const,
|
||||
retentionPolicy: Object.freeze({
|
||||
revision: retention.revision,
|
||||
retentionMs: retention.retentionMs as number,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function response(
|
||||
statusCode: number,
|
||||
body: Readonly<Record<string, unknown>>,
|
||||
): ClusterControlAdmissionResponse {
|
||||
return Object.freeze({ statusCode, body: Object.freeze(body) });
|
||||
}
|
||||
|
||||
function errorCode(error: unknown): string | null {
|
||||
if (
|
||||
!error ||
|
||||
typeof error !== 'object' ||
|
||||
!('code' in error) ||
|
||||
typeof error.code !== 'string'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return error.code;
|
||||
}
|
||||
|
||||
function executionError(error: unknown): ClusterControlAdmissionResponse {
|
||||
const code = errorCode(error);
|
||||
if (
|
||||
code === 'PLUGIN_PACKAGE_PROMPT_ADMISSION_NOT_ALLOWED' ||
|
||||
code === 'PLUGIN_PACKAGE_PROMPT_ADMISSION_CONFLICT' ||
|
||||
code === 'PLUGIN_PACKAGE_PROMPT_EXECUTION_IN_PROGRESS' ||
|
||||
code === 'PLUGIN_PACKAGE_PROMPT_RESOLUTION_REQUIRED' ||
|
||||
code === 'MODEL_INVOCATION_CONFLICT' ||
|
||||
code === 'MODEL_INVOCATION_REPLAY_BLOCKED'
|
||||
) {
|
||||
return response(409, { code: 'prompt_execution_conflict' });
|
||||
}
|
||||
if (code === 'MODEL_GATEWAY_BUSY' || code === 'MODEL_PROJECT_QUOTA_EXCEEDED') {
|
||||
return response(429, { code: 'prompt_execution_capacity_exceeded' });
|
||||
}
|
||||
if (code === 'MODEL_POLICY_DENIED' || code === 'MODEL_BUDGET_EXCEEDED') {
|
||||
return response(422, { code: 'prompt_execution_policy_rejected' });
|
||||
}
|
||||
if (code === 'MODEL_INVOCATION_DEADLINE_EXCEEDED') {
|
||||
return response(504, { code: 'prompt_execution_deadline_exceeded' });
|
||||
}
|
||||
if (code === 'MODEL_INVOCATION_ABORTED') {
|
||||
return response(408, { code: 'prompt_execution_aborted' });
|
||||
}
|
||||
if (code === 'PLUGIN_PACKAGE_PROMPT_EXECUTION_PLAN_INVALID') {
|
||||
return response(400, { code: 'invalid_prompt_execution_request' });
|
||||
}
|
||||
return response(503, { code: 'prompt_execution_unavailable' });
|
||||
}
|
||||
|
||||
function parseBody(value: unknown, maximumExecutionMs: number) {
|
||||
const body = dataRecord(value);
|
||||
exactKeys(
|
||||
body,
|
||||
[
|
||||
'schema',
|
||||
'requestId',
|
||||
'traceId',
|
||||
'parameters',
|
||||
'provider',
|
||||
'model',
|
||||
'maxOutputTokens',
|
||||
'timeoutMs',
|
||||
],
|
||||
['output', 'temperature'],
|
||||
);
|
||||
if (body.schema !== CLUSTER_PLUGIN_PACKAGE_PROMPT_EXECUTION_REQUEST_SCHEMA) {
|
||||
return invalid();
|
||||
}
|
||||
const temperature = body.temperature;
|
||||
const output =
|
||||
body.output === undefined ? undefined : outputIntent(body.output);
|
||||
if (
|
||||
temperature !== undefined &&
|
||||
(typeof temperature !== 'number' ||
|
||||
!Number.isFinite(temperature) ||
|
||||
temperature < 0 ||
|
||||
temperature > 2)
|
||||
) {
|
||||
return invalid();
|
||||
}
|
||||
return Object.freeze({
|
||||
requestId: identifier(body.requestId),
|
||||
traceId: identifier(body.traceId),
|
||||
parameters: parameters(body.parameters),
|
||||
provider: identifier(body.provider),
|
||||
model: identifier(body.model),
|
||||
maxOutputTokens: positiveInteger(
|
||||
body.maxOutputTokens,
|
||||
CLUSTER_PLUGIN_PACKAGE_PROMPT_EXECUTION_LIMITS.maxOutputTokens,
|
||||
),
|
||||
timeoutMs: positiveInteger(body.timeoutMs, maximumExecutionMs),
|
||||
...(output === undefined ? {} : { output }),
|
||||
...(temperature === undefined ? {} : { temperature }),
|
||||
});
|
||||
}
|
||||
|
||||
export function createClusterControlPluginPackagePromptExecutionRoute(
|
||||
capability: ClusterPluginPackagePromptExecutionCapability,
|
||||
options: ClusterPluginPackagePromptExecutionRouteOptions = {},
|
||||
): Readonly<ClusterControlRouteDefinition> {
|
||||
if (!capability || typeof capability.execute !== 'function') {
|
||||
throw new TypeError('Cluster-control Prompt execution capability is invalid');
|
||||
}
|
||||
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
||||
throw new TypeError('Cluster-control Prompt execution route options are invalid');
|
||||
}
|
||||
const maximumExecutionMs =
|
||||
options.maxExecutionMs ??
|
||||
CLUSTER_PLUGIN_PACKAGE_PROMPT_EXECUTION_LIMITS.maxExecutionMs;
|
||||
if (
|
||||
!Number.isSafeInteger(maximumExecutionMs) ||
|
||||
maximumExecutionMs < 1 ||
|
||||
maximumExecutionMs >
|
||||
CLUSTER_PLUGIN_PACKAGE_PROMPT_EXECUTION_LIMITS.maxExecutionMs ||
|
||||
(options.now !== undefined && typeof options.now !== 'function') ||
|
||||
(options.createEventId !== undefined &&
|
||||
typeof options.createEventId !== 'function')
|
||||
) {
|
||||
throw new TypeError('Cluster-control Prompt execution route options are invalid');
|
||||
}
|
||||
const now = options.now ?? Date.now;
|
||||
const createEventId = options.createEventId ?? randomUUID;
|
||||
return Object.freeze({
|
||||
...CLUSTER_CONTROL_PLUGIN_PACKAGE_PROMPT_EXECUTION_ROUTE,
|
||||
async handle(
|
||||
authorized: ClusterControlAuthorizedOperationRequest,
|
||||
routeParameters: ClusterControlRouteParameters,
|
||||
) {
|
||||
let body;
|
||||
try {
|
||||
body = parseBody(authorized.request.body, maximumExecutionMs);
|
||||
} catch {
|
||||
return response(400, { code: 'invalid_prompt_execution_request' });
|
||||
}
|
||||
const projectId = authorized.projectId;
|
||||
const packageName = routeParameters.packageName;
|
||||
const promptId = routeParameters.promptId;
|
||||
const fence = authorized.policyFence;
|
||||
const plannedAtMs = now();
|
||||
const auditEventId = createEventId();
|
||||
if (
|
||||
projectId === null ||
|
||||
typeof packageName !== 'string' ||
|
||||
!PACKAGE_NAME.test(packageName) ||
|
||||
typeof promptId !== 'string' ||
|
||||
!IDENTIFIER.test(promptId) ||
|
||||
!fence ||
|
||||
fence.bindingVersion === null ||
|
||||
!Number.isSafeInteger(plannedAtMs) ||
|
||||
plannedAtMs < 0 ||
|
||||
typeof auditEventId !== 'string' ||
|
||||
!UUID_V4.test(auditEventId)
|
||||
) {
|
||||
return response(503, { code: 'prompt_execution_unavailable' });
|
||||
}
|
||||
try {
|
||||
const result = await capability.execute({
|
||||
projectId,
|
||||
packageName,
|
||||
promptId,
|
||||
requestId: body.requestId,
|
||||
traceId: body.traceId,
|
||||
auditEventId,
|
||||
principal: authorized.principal,
|
||||
policyFence: Object.freeze({
|
||||
projectVersion: fence.projectVersion,
|
||||
bindingVersion: fence.bindingVersion,
|
||||
}),
|
||||
parameters: body.parameters,
|
||||
provider: body.provider,
|
||||
model: body.model,
|
||||
maxOutputTokens: body.maxOutputTokens,
|
||||
...(body.temperature === undefined
|
||||
? {}
|
||||
: { temperature: body.temperature }),
|
||||
...(body.output === undefined ? {} : { output: body.output }),
|
||||
plannedAtMs,
|
||||
deadlineAtMs: plannedAtMs + body.timeoutMs,
|
||||
signal: authorized.request.signal,
|
||||
});
|
||||
return response(200, {
|
||||
schema: CLUSTER_PLUGIN_PACKAGE_PROMPT_EXECUTION_RESPONSE_SCHEMA,
|
||||
status: result.status,
|
||||
replayed: result.status === 'existing',
|
||||
requestId: result.admission.requestId,
|
||||
invocationId: result.admission.invocationId,
|
||||
runId: result.admission.runId,
|
||||
stepRunId: result.admission.stepRunId,
|
||||
runStatus: result.finalization.runStatus,
|
||||
result: result.result,
|
||||
...(result.outputArtifact === undefined
|
||||
? {}
|
||||
: { outputArtifact: result.outputArtifact }),
|
||||
});
|
||||
} catch (error) {
|
||||
return executionError(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
// Plugin Package Prompt owns its capability-free durable output projection.
|
||||
import type { SecurityPrincipal } from '@qinglong/runtime-core/security';
|
||||
|
||||
import type { ClusterControlAdmissionResponse } from '../../transport/httpSurface';
|
||||
import type {
|
||||
ClusterControlAuthorizedOperationRequest,
|
||||
ClusterControlRouteDefinition,
|
||||
ClusterControlRouteParameters,
|
||||
} from '../../transport/routeRegistry';
|
||||
|
||||
export const CLUSTER_PLUGIN_PACKAGE_PROMPT_OUTPUT_READ_RESPONSE_SCHEMA =
|
||||
'qinglong/cluster-plugin-package-prompt-output-read-response@v1' as const;
|
||||
export const CLUSTER_CONTROL_PLUGIN_PACKAGE_PROMPT_OUTPUT_READ_ROUTE =
|
||||
Object.freeze({
|
||||
method: 'GET' as const,
|
||||
path: '/api/v3/projects/{projectId}/runs/{runId}/prompt-output-artifacts/{artifactId}',
|
||||
operationId: 'prompt.output.read',
|
||||
permission: 'artifact.read',
|
||||
projectParameter: 'projectId',
|
||||
allowedQuery: Object.freeze(['artifact_digest']),
|
||||
});
|
||||
|
||||
export interface ClusterPluginPackagePromptOutputReadCapability {
|
||||
read(command: Readonly<{
|
||||
principal: Readonly<SecurityPrincipal>;
|
||||
projectId: string;
|
||||
runId: string;
|
||||
artifactId: string;
|
||||
artifactDigest: string;
|
||||
}>): Promise<unknown>;
|
||||
}
|
||||
|
||||
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 MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/;
|
||||
const DIGEST = /^[0-9a-f]{64}$/;
|
||||
const KEY_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
||||
const FINISH_REASONS = new Set([
|
||||
'stop',
|
||||
'length',
|
||||
'content_filter',
|
||||
'tool_call',
|
||||
'unknown',
|
||||
]);
|
||||
|
||||
function response(
|
||||
statusCode: number,
|
||||
body: Readonly<Record<string, unknown>>,
|
||||
): ClusterControlAdmissionResponse {
|
||||
return Object.freeze({ statusCode, body: Object.freeze(body) });
|
||||
}
|
||||
|
||||
function exactRecord(
|
||||
value: unknown,
|
||||
required: readonly string[],
|
||||
optional: readonly string[] = [],
|
||||
): Record<string, unknown> | null {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
||||
const record = value as Record<string, unknown>;
|
||||
const keys = Object.keys(record).sort();
|
||||
const expected = [...required, ...optional.filter((key) => key in record)].sort();
|
||||
return keys.length === expected.length &&
|
||||
keys.every((key, index) => key === expected[index])
|
||||
? record
|
||||
: null;
|
||||
}
|
||||
|
||||
function nonNegativeInteger(value: unknown): value is number {
|
||||
return Number.isSafeInteger(value) && (value as number) >= 0;
|
||||
}
|
||||
|
||||
function availableView(
|
||||
value: unknown,
|
||||
expected: Readonly<{
|
||||
projectId: string;
|
||||
runId: string;
|
||||
artifactId: string;
|
||||
artifactDigest: string;
|
||||
}>,
|
||||
): Readonly<Record<string, unknown>> | null {
|
||||
const resultEnvelope = exactRecord(value, [
|
||||
'schema',
|
||||
'status',
|
||||
'reference',
|
||||
'result',
|
||||
]);
|
||||
if (
|
||||
!resultEnvelope ||
|
||||
resultEnvelope.schema !==
|
||||
'qinglong/plugin-package-prompt-output-read-result@v1' ||
|
||||
resultEnvelope.status !== 'available'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const reference = exactRecord(resultEnvelope.reference, [
|
||||
'algorithm',
|
||||
'artifactDigest',
|
||||
'artifactId',
|
||||
'contentDigest',
|
||||
'invocationId',
|
||||
'keyId',
|
||||
'outputBytes',
|
||||
'projectId',
|
||||
'retentionEligibleAtMs',
|
||||
'retentionPolicyDigest',
|
||||
'runId',
|
||||
'schema',
|
||||
'stepRunId',
|
||||
]);
|
||||
const result = exactRecord(resultEnvelope.result, [
|
||||
'finishReason',
|
||||
'model',
|
||||
'provider',
|
||||
'text',
|
||||
'usage',
|
||||
]);
|
||||
const usage = result ? exactRecord(result.usage, [
|
||||
'inputTokens',
|
||||
'outputTokens',
|
||||
'totalTokens',
|
||||
], ['costMicros']) : null;
|
||||
if (
|
||||
!reference ||
|
||||
!result ||
|
||||
!usage ||
|
||||
reference.schema !==
|
||||
'qinglong/plugin-package-prompt-output-artifact-reference@v1' ||
|
||||
reference.algorithm !== 'aes-256-gcm' ||
|
||||
reference.projectId !== expected.projectId ||
|
||||
reference.runId !== expected.runId ||
|
||||
reference.artifactId !== expected.artifactId ||
|
||||
reference.artifactDigest !== expected.artifactDigest ||
|
||||
typeof reference.stepRunId !== 'string' ||
|
||||
!IDENTITY.test(reference.stepRunId) ||
|
||||
typeof reference.invocationId !== 'string' ||
|
||||
!IDENTITY.test(reference.invocationId) ||
|
||||
typeof reference.contentDigest !== 'string' ||
|
||||
!DIGEST.test(reference.contentDigest) ||
|
||||
!nonNegativeInteger(reference.outputBytes) ||
|
||||
reference.outputBytes > 1024 * 1024 ||
|
||||
typeof reference.retentionPolicyDigest !== 'string' ||
|
||||
!DIGEST.test(reference.retentionPolicyDigest) ||
|
||||
!nonNegativeInteger(reference.retentionEligibleAtMs) ||
|
||||
typeof reference.keyId !== 'string' ||
|
||||
!KEY_ID.test(reference.keyId) ||
|
||||
typeof result.provider !== 'string' ||
|
||||
!MODEL_ID.test(result.provider) ||
|
||||
typeof result.model !== 'string' ||
|
||||
!MODEL_ID.test(result.model) ||
|
||||
typeof result.text !== 'string' ||
|
||||
Buffer.byteLength(result.text, 'utf8') > 1024 * 1024 ||
|
||||
!FINISH_REASONS.has(result.finishReason as string) ||
|
||||
!nonNegativeInteger(usage.inputTokens) ||
|
||||
!nonNegativeInteger(usage.outputTokens) ||
|
||||
!nonNegativeInteger(usage.totalTokens) ||
|
||||
usage.totalTokens !== usage.inputTokens + usage.outputTokens ||
|
||||
(usage.costMicros !== undefined && !nonNegativeInteger(usage.costMicros))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return Object.freeze({
|
||||
schema: CLUSTER_PLUGIN_PACKAGE_PROMPT_OUTPUT_READ_RESPONSE_SCHEMA,
|
||||
status: 'available',
|
||||
reference: Object.freeze({ ...reference }),
|
||||
result: Object.freeze({
|
||||
provider: result.provider,
|
||||
model: result.model,
|
||||
text: result.text,
|
||||
finishReason: result.finishReason,
|
||||
usage: Object.freeze({ ...usage }),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export function createClusterControlPluginPackagePromptOutputReadRoute(
|
||||
capability: ClusterPluginPackagePromptOutputReadCapability,
|
||||
): Readonly<ClusterControlRouteDefinition> {
|
||||
if (!capability || typeof capability.read !== 'function') {
|
||||
throw new TypeError('Cluster-control Prompt output read capability is invalid');
|
||||
}
|
||||
return Object.freeze({
|
||||
...CLUSTER_CONTROL_PLUGIN_PACKAGE_PROMPT_OUTPUT_READ_ROUTE,
|
||||
async handle(
|
||||
authorized: ClusterControlAuthorizedOperationRequest,
|
||||
parameters: ClusterControlRouteParameters,
|
||||
) {
|
||||
const artifactDigestValues =
|
||||
authorized.request.query.artifact_digest;
|
||||
if (
|
||||
authorized.request.body !== null ||
|
||||
authorized.projectId === null ||
|
||||
typeof parameters.runId !== 'string' ||
|
||||
!RUN_ID.test(parameters.runId) ||
|
||||
typeof parameters.artifactId !== 'string' ||
|
||||
!IDENTITY.test(parameters.artifactId) ||
|
||||
!Array.isArray(artifactDigestValues) ||
|
||||
artifactDigestValues.length !== 1 ||
|
||||
typeof artifactDigestValues[0] !== 'string' ||
|
||||
!DIGEST.test(artifactDigestValues[0])
|
||||
) {
|
||||
return response(400, { code: 'invalid_prompt_output_read_request' });
|
||||
}
|
||||
const expected = Object.freeze({
|
||||
projectId: authorized.projectId,
|
||||
runId: parameters.runId,
|
||||
artifactId: parameters.artifactId,
|
||||
artifactDigest: artifactDigestValues[0],
|
||||
});
|
||||
try {
|
||||
const result = await capability.read({
|
||||
principal: authorized.principal,
|
||||
...expected,
|
||||
});
|
||||
const notFound = exactRecord(result, ['schema', 'status']);
|
||||
if (
|
||||
notFound &&
|
||||
notFound.schema ===
|
||||
'qinglong/plugin-package-prompt-output-read-result@v1' &&
|
||||
notFound.status === 'not_found'
|
||||
) {
|
||||
return response(404, { code: 'prompt_output_not_found' });
|
||||
}
|
||||
const view = availableView(result, expected);
|
||||
return view
|
||||
? response(200, view)
|
||||
: response(503, { code: 'prompt_output_read_unavailable' });
|
||||
} catch {
|
||||
return response(503, { code: 'prompt_output_read_unavailable' });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Plugin Package Prompt owns its stable execution and output-read route surface.
|
||||
export * from './pluginPackagePromptExecutionRoute';
|
||||
export * from './pluginPackagePromptCatalogRoute';
|
||||
export * from './pluginPackagePromptExecutionInspectionRoute';
|
||||
export * from './pluginPackagePromptExecutionOutputReadRoute';
|
||||
export * from './pluginPackagePromptOutputReadRoute';
|
||||
+535
@@ -0,0 +1,535 @@
|
||||
// Plugin Package Workflow owns inspection and durable authorized admission.
|
||||
import type {
|
||||
SecurityPolicyFence,
|
||||
SecurityPrincipal,
|
||||
} from '@qinglong/runtime-core/security';
|
||||
import { normalizeSecurityAuditRecord } from '@qinglong/runtime-core/security-audit';
|
||||
import type {
|
||||
PluginPackageAutomationPublication,
|
||||
PluginPackageAutomationPublicationRepository,
|
||||
} from '@qinglong/runtime-core/plugin-package-automation-publication';
|
||||
import type {
|
||||
PluginPackageMaterializedRevision,
|
||||
PluginPackageMaterializedRevisionRepository,
|
||||
PluginPackageWorkflowResource,
|
||||
} from '@qinglong/runtime-core/plugin-package-resource-materialization';
|
||||
import type {
|
||||
PluginPackageWorkflowAdministrationRepository,
|
||||
PluginPackageWorkflowRunEventListRepository,
|
||||
PluginPackageWorkflowRunEventListResult,
|
||||
PluginPackageWorkflowRunInspectionRepository,
|
||||
PluginPackageWorkflowRunInspectionResult,
|
||||
PluginPackageWorkflowRunListRepository,
|
||||
PluginPackageWorkflowRunListResult,
|
||||
PluginPackageWorkflowStepRunListRepository,
|
||||
PluginPackageWorkflowStepRunListResult,
|
||||
} from '@qinglong/runtime-core/plugin-package-workflow-administration';
|
||||
import type {
|
||||
ClusterRunCancellationRepository,
|
||||
ClusterRunCancellationResult,
|
||||
} from '@qinglong/runtime-core/cluster-run-cancellation';
|
||||
import {
|
||||
createPluginPackageWorkflowExecutionPlan,
|
||||
type PluginPackageWorkflowAdmissionReceipt,
|
||||
type PluginPackageWorkflowExecutionPlan,
|
||||
} from '@qinglong/runtime-core/plugin-package-workflow-execution-plan';
|
||||
import {
|
||||
TaskSpecSemanticRegistry,
|
||||
createBuiltInTaskSpecSemanticRegistry,
|
||||
} from '@qinglong/runtime-core/task-spec-semantic';
|
||||
|
||||
export interface ClusterPluginPackageWorkflowSummary {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly enabled: boolean;
|
||||
readonly steps: readonly Readonly<{
|
||||
id: string;
|
||||
task: string;
|
||||
needs: readonly string[];
|
||||
}>[];
|
||||
}
|
||||
|
||||
export interface StartClusterPluginPackageWorkflowCommand {
|
||||
readonly projectId: string;
|
||||
readonly packageName: string;
|
||||
readonly workflowId: string;
|
||||
readonly planId: string;
|
||||
readonly runId: string;
|
||||
readonly stepRunIds: Readonly<Record<string, string>>;
|
||||
readonly principal: Readonly<SecurityPrincipal>;
|
||||
readonly policyFence: Readonly<SecurityPolicyFence>;
|
||||
readonly plannedAtMs: number;
|
||||
}
|
||||
|
||||
export interface CancelClusterPluginPackageWorkflowCommand {
|
||||
readonly projectId: string;
|
||||
readonly packageName: string;
|
||||
readonly workflowId: string;
|
||||
readonly runId: string;
|
||||
readonly mutationId: string;
|
||||
readonly eventId: string;
|
||||
readonly principal: Readonly<SecurityPrincipal>;
|
||||
readonly policyFence: Readonly<SecurityPolicyFence>;
|
||||
}
|
||||
|
||||
export interface InspectClusterPluginPackageWorkflowRunCommand {
|
||||
readonly projectId: string;
|
||||
readonly packageName: string;
|
||||
readonly workflowId: string;
|
||||
readonly runId: string;
|
||||
readonly requestId: string;
|
||||
readonly auditEventId: string;
|
||||
readonly principal: Readonly<SecurityPrincipal>;
|
||||
readonly policyFence: Readonly<SecurityPolicyFence>;
|
||||
readonly observedAtMs: number;
|
||||
}
|
||||
|
||||
export interface ListClusterPluginPackageWorkflowRunsCommand {
|
||||
readonly projectId: string;
|
||||
readonly packageName: string;
|
||||
readonly workflowId: string;
|
||||
readonly limit: number;
|
||||
readonly after: Readonly<{ admittedAtMs: number; runId: string }> | null;
|
||||
readonly requestId: string;
|
||||
readonly auditEventId: string;
|
||||
readonly principal: Readonly<SecurityPrincipal>;
|
||||
readonly policyFence: Readonly<SecurityPolicyFence>;
|
||||
readonly observedAtMs: number;
|
||||
}
|
||||
|
||||
export interface ListClusterPluginPackageWorkflowStepRunsCommand {
|
||||
readonly projectId: string;
|
||||
readonly packageName: string;
|
||||
readonly workflowId: string;
|
||||
readonly runId: string;
|
||||
readonly limit: number;
|
||||
readonly after: Readonly<{ stepKey: string; id: string }> | null;
|
||||
readonly requestId: string;
|
||||
readonly auditEventId: string;
|
||||
readonly principal: Readonly<SecurityPrincipal>;
|
||||
readonly policyFence: Readonly<SecurityPolicyFence>;
|
||||
readonly observedAtMs: number;
|
||||
}
|
||||
|
||||
export interface ListClusterPluginPackageWorkflowRunEventsCommand {
|
||||
readonly projectId: string;
|
||||
readonly packageName: string;
|
||||
readonly workflowId: string;
|
||||
readonly runId: string;
|
||||
readonly limit: number;
|
||||
readonly afterSequence: number;
|
||||
readonly requestId: string;
|
||||
readonly auditEventId: string;
|
||||
readonly principal: Readonly<SecurityPrincipal>;
|
||||
readonly policyFence: Readonly<SecurityPolicyFence>;
|
||||
readonly observedAtMs: number;
|
||||
}
|
||||
|
||||
export interface ClusterPluginPackageWorkflowAdministrationCapability {
|
||||
inspect(
|
||||
projectId: string,
|
||||
packageName: string,
|
||||
): Promise<
|
||||
Readonly<{
|
||||
found: boolean;
|
||||
publicationState: PluginPackageAutomationPublication['state'] | null;
|
||||
workflows: readonly Readonly<ClusterPluginPackageWorkflowSummary>[];
|
||||
}>
|
||||
>;
|
||||
start(command: Readonly<StartClusterPluginPackageWorkflowCommand>): Promise<
|
||||
Readonly<{
|
||||
status: 'created' | 'existing';
|
||||
plan: Readonly<PluginPackageWorkflowExecutionPlan>;
|
||||
receipt: Readonly<PluginPackageWorkflowAdmissionReceipt>;
|
||||
}>
|
||||
>;
|
||||
cancel(
|
||||
command: Readonly<CancelClusterPluginPackageWorkflowCommand>,
|
||||
): Promise<Readonly<ClusterRunCancellationResult>>;
|
||||
inspectRun(
|
||||
command: Readonly<InspectClusterPluginPackageWorkflowRunCommand>,
|
||||
): Promise<Readonly<PluginPackageWorkflowRunInspectionResult>>;
|
||||
listRuns(
|
||||
command: Readonly<ListClusterPluginPackageWorkflowRunsCommand>,
|
||||
): Promise<Readonly<PluginPackageWorkflowRunListResult>>;
|
||||
listStepRuns(
|
||||
command: Readonly<ListClusterPluginPackageWorkflowStepRunsCommand>,
|
||||
): Promise<Readonly<PluginPackageWorkflowStepRunListResult>>;
|
||||
listRunEvents(
|
||||
command: Readonly<ListClusterPluginPackageWorkflowRunEventsCommand>,
|
||||
): Promise<Readonly<PluginPackageWorkflowRunEventListResult>>;
|
||||
}
|
||||
|
||||
export class ClusterPluginPackageWorkflowNotFoundError extends Error {
|
||||
readonly code = 'CLUSTER_PLUGIN_PACKAGE_WORKFLOW_NOT_FOUND';
|
||||
|
||||
constructor() {
|
||||
super('Active Plugin Package Workflow is not available');
|
||||
this.name = 'ClusterPluginPackageWorkflowNotFoundError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ClusterPluginPackageWorkflowConflictError extends Error {
|
||||
readonly code = 'CLUSTER_PLUGIN_PACKAGE_WORKFLOW_CONFLICT';
|
||||
|
||||
constructor() {
|
||||
super('Plugin Package Workflow request conflicts with durable identity');
|
||||
this.name = 'ClusterPluginPackageWorkflowConflictError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ClusterPluginPackageWorkflowUnavailableError extends Error {
|
||||
readonly code = 'CLUSTER_PLUGIN_PACKAGE_WORKFLOW_UNAVAILABLE';
|
||||
|
||||
constructor() {
|
||||
super('Plugin Package Workflow administration is unavailable');
|
||||
this.name = 'ClusterPluginPackageWorkflowUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
function summary(
|
||||
workflow: Readonly<PluginPackageWorkflowResource>,
|
||||
): Readonly<ClusterPluginPackageWorkflowSummary> {
|
||||
return Object.freeze({
|
||||
id: workflow.id,
|
||||
name: workflow.name,
|
||||
enabled: workflow.enabled,
|
||||
steps: Object.freeze(
|
||||
workflow.steps.map((step) =>
|
||||
Object.freeze({
|
||||
id: step.id,
|
||||
task: step.task,
|
||||
needs: Object.freeze([...step.needs]),
|
||||
}),
|
||||
),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
function sameReplay(
|
||||
plan: Readonly<PluginPackageWorkflowExecutionPlan>,
|
||||
command: Readonly<StartClusterPluginPackageWorkflowCommand>,
|
||||
): boolean {
|
||||
const requested = Object.entries(command.stepRunIds).sort(([a], [b]) =>
|
||||
a.localeCompare(b),
|
||||
);
|
||||
const stored = plan.steps
|
||||
.map((step) => [step.stepKey, step.stepRunId] as const)
|
||||
.sort(([a], [b]) => a.localeCompare(b));
|
||||
return (
|
||||
plan.planId === command.planId &&
|
||||
plan.runId === command.runId &&
|
||||
plan.target.projectId === command.projectId &&
|
||||
plan.target.packageName === command.packageName &&
|
||||
plan.target.workflowId === command.workflowId &&
|
||||
stored.length === requested.length &&
|
||||
stored.every(
|
||||
([key, id], index) =>
|
||||
key === requested[index]?.[0] && id === requested[index]?.[1],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function createClusterPluginPackageWorkflowAdministrationCapability(
|
||||
publications: Pick<
|
||||
PluginPackageAutomationPublicationRepository,
|
||||
'findCurrent'
|
||||
>,
|
||||
revisions: Pick<PluginPackageMaterializedRevisionRepository, 'find'>,
|
||||
admissions: PluginPackageWorkflowAdministrationRepository,
|
||||
runInspections: PluginPackageWorkflowRunInspectionRepository,
|
||||
runLists: PluginPackageWorkflowRunListRepository,
|
||||
stepRunLists: PluginPackageWorkflowStepRunListRepository,
|
||||
runEventLists: PluginPackageWorkflowRunEventListRepository,
|
||||
cancellations: ClusterRunCancellationRepository,
|
||||
taskSpecSemanticRegistry: TaskSpecSemanticRegistry = createBuiltInTaskSpecSemanticRegistry(),
|
||||
): ClusterPluginPackageWorkflowAdministrationCapability {
|
||||
if (
|
||||
!publications ||
|
||||
typeof publications.findCurrent !== 'function' ||
|
||||
!revisions ||
|
||||
typeof revisions.find !== 'function' ||
|
||||
!admissions ||
|
||||
typeof admissions.findPlanByPlanId !== 'function' ||
|
||||
typeof admissions.admitAuthorized !== 'function' ||
|
||||
!runInspections ||
|
||||
typeof runInspections.inspectRunAuthorized !== 'function' ||
|
||||
!runLists ||
|
||||
typeof runLists.listRunsAuthorized !== 'function' ||
|
||||
!stepRunLists ||
|
||||
typeof stepRunLists.listStepRunsAuthorized !== 'function' ||
|
||||
!runEventLists ||
|
||||
typeof runEventLists.listRunEventsAuthorized !== 'function' ||
|
||||
!cancellations ||
|
||||
typeof cancellations.requestUserCancellation !== 'function' ||
|
||||
!(taskSpecSemanticRegistry instanceof TaskSpecSemanticRegistry)
|
||||
) {
|
||||
throw new TypeError(
|
||||
'Cluster Plugin Package Workflow administration dependencies are invalid',
|
||||
);
|
||||
}
|
||||
|
||||
async function currentTarget(
|
||||
projectId: string,
|
||||
packageName: string,
|
||||
): Promise<Readonly<{
|
||||
publication: Readonly<PluginPackageAutomationPublication>;
|
||||
revision: Readonly<PluginPackageMaterializedRevision>;
|
||||
}> | null> {
|
||||
try {
|
||||
const publication = await publications.findCurrent(
|
||||
projectId,
|
||||
packageName,
|
||||
);
|
||||
if (!publication) return null;
|
||||
const revision = await revisions.find(
|
||||
publication.target.generationDigest,
|
||||
);
|
||||
if (
|
||||
!revision ||
|
||||
revision.revisionDigest !==
|
||||
publication.target.materializedRevisionDigest
|
||||
) {
|
||||
throw new ClusterPluginPackageWorkflowUnavailableError();
|
||||
}
|
||||
return Object.freeze({ publication, revision });
|
||||
} catch (error) {
|
||||
if (error instanceof ClusterPluginPackageWorkflowUnavailableError) {
|
||||
throw error;
|
||||
}
|
||||
throw new ClusterPluginPackageWorkflowUnavailableError();
|
||||
}
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
async inspect(projectId: string, packageName: string) {
|
||||
const target = await currentTarget(projectId, packageName);
|
||||
return target
|
||||
? Object.freeze({
|
||||
found: true,
|
||||
publicationState: target.publication.state,
|
||||
workflows: Object.freeze(
|
||||
target.publication.definitions.workflows.map(summary),
|
||||
),
|
||||
})
|
||||
: Object.freeze({
|
||||
found: false,
|
||||
publicationState: null,
|
||||
workflows: Object.freeze([]),
|
||||
});
|
||||
},
|
||||
|
||||
async start(command: Readonly<StartClusterPluginPackageWorkflowCommand>) {
|
||||
let plan = await admissions.findPlanByPlanId(command.planId);
|
||||
if (plan) {
|
||||
if (!sameReplay(plan, command)) {
|
||||
throw new ClusterPluginPackageWorkflowConflictError();
|
||||
}
|
||||
} else {
|
||||
const target = await currentTarget(
|
||||
command.projectId,
|
||||
command.packageName,
|
||||
);
|
||||
const workflow = target?.publication.definitions.workflows.find(
|
||||
({ id }) => id === command.workflowId,
|
||||
);
|
||||
if (
|
||||
!target ||
|
||||
target.publication.state !== 'active' ||
|
||||
!workflow?.enabled
|
||||
) {
|
||||
throw new ClusterPluginPackageWorkflowNotFoundError();
|
||||
}
|
||||
try {
|
||||
plan = createPluginPackageWorkflowExecutionPlan({
|
||||
planId: command.planId,
|
||||
runId: command.runId,
|
||||
workflowId: command.workflowId,
|
||||
stepRunIds: command.stepRunIds,
|
||||
publication: target.publication,
|
||||
revision: target.revision,
|
||||
taskSpecSemanticRegistry,
|
||||
plannedAtMs: command.plannedAtMs,
|
||||
});
|
||||
} catch {
|
||||
throw new ClusterPluginPackageWorkflowConflictError();
|
||||
}
|
||||
}
|
||||
if (command.policyFence.bindingVersion === null) {
|
||||
throw new ClusterPluginPackageWorkflowUnavailableError();
|
||||
}
|
||||
const admitted = await admissions.admitAuthorized({
|
||||
plan,
|
||||
actor: command.principal.subject,
|
||||
fence: {
|
||||
projectVersion: command.policyFence.projectVersion,
|
||||
bindingVersion: command.policyFence.bindingVersion,
|
||||
},
|
||||
audit: normalizeSecurityAuditRecord({
|
||||
eventId: command.planId,
|
||||
requestId: command.planId,
|
||||
operationId: 'workflow.start',
|
||||
projectId: command.projectId,
|
||||
subject: command.principal.subject,
|
||||
authenticationId: command.principal.authenticationId,
|
||||
outcome: 'allowed',
|
||||
reasons: ['project_policy_allowed'],
|
||||
fence: command.policyFence,
|
||||
occurredAtMs: plan.plannedAtMs,
|
||||
}),
|
||||
});
|
||||
return Object.freeze({
|
||||
status: admitted.status,
|
||||
plan,
|
||||
receipt: admitted.receipt,
|
||||
});
|
||||
},
|
||||
|
||||
async cancel(command: Readonly<CancelClusterPluginPackageWorkflowCommand>) {
|
||||
if (command.policyFence.bindingVersion === null) {
|
||||
throw new ClusterPluginPackageWorkflowUnavailableError();
|
||||
}
|
||||
return cancellations.requestUserCancellation({
|
||||
projectId: command.projectId,
|
||||
runId: command.runId,
|
||||
mutationId: command.mutationId,
|
||||
eventId: command.eventId,
|
||||
subject: command.principal.subject,
|
||||
policyFence: command.policyFence,
|
||||
workflowTarget: {
|
||||
packageName: command.packageName,
|
||||
workflowId: command.workflowId,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async inspectRun(
|
||||
command: Readonly<InspectClusterPluginPackageWorkflowRunCommand>,
|
||||
) {
|
||||
if (command.policyFence.bindingVersion === null) {
|
||||
throw new ClusterPluginPackageWorkflowUnavailableError();
|
||||
}
|
||||
return runInspections.inspectRunAuthorized({
|
||||
projectId: command.projectId,
|
||||
packageName: command.packageName,
|
||||
workflowId: command.workflowId,
|
||||
runId: command.runId,
|
||||
actor: command.principal.subject,
|
||||
fence: {
|
||||
projectVersion: command.policyFence.projectVersion,
|
||||
bindingVersion: command.policyFence.bindingVersion,
|
||||
},
|
||||
audit: normalizeSecurityAuditRecord({
|
||||
eventId: command.auditEventId,
|
||||
requestId: command.requestId,
|
||||
operationId: 'workflow.run.read',
|
||||
projectId: command.projectId,
|
||||
subject: command.principal.subject,
|
||||
authenticationId: command.principal.authenticationId,
|
||||
outcome: 'allowed',
|
||||
reasons: ['project_policy_allowed'],
|
||||
fence: command.policyFence,
|
||||
occurredAtMs: command.observedAtMs,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
async listRuns(
|
||||
command: Readonly<ListClusterPluginPackageWorkflowRunsCommand>,
|
||||
) {
|
||||
if (command.policyFence.bindingVersion === null) {
|
||||
throw new ClusterPluginPackageWorkflowUnavailableError();
|
||||
}
|
||||
return runLists.listRunsAuthorized({
|
||||
projectId: command.projectId,
|
||||
packageName: command.packageName,
|
||||
workflowId: command.workflowId,
|
||||
limit: command.limit,
|
||||
after: command.after,
|
||||
actor: command.principal.subject,
|
||||
fence: {
|
||||
projectVersion: command.policyFence.projectVersion,
|
||||
bindingVersion: command.policyFence.bindingVersion,
|
||||
},
|
||||
audit: normalizeSecurityAuditRecord({
|
||||
eventId: command.auditEventId,
|
||||
requestId: command.requestId,
|
||||
operationId: 'workflow.run.list',
|
||||
projectId: command.projectId,
|
||||
subject: command.principal.subject,
|
||||
authenticationId: command.principal.authenticationId,
|
||||
outcome: 'allowed',
|
||||
reasons: ['project_policy_allowed'],
|
||||
fence: command.policyFence,
|
||||
occurredAtMs: command.observedAtMs,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
async listStepRuns(
|
||||
command: Readonly<ListClusterPluginPackageWorkflowStepRunsCommand>,
|
||||
) {
|
||||
if (command.policyFence.bindingVersion === null) {
|
||||
throw new ClusterPluginPackageWorkflowUnavailableError();
|
||||
}
|
||||
return stepRunLists.listStepRunsAuthorized({
|
||||
projectId: command.projectId,
|
||||
packageName: command.packageName,
|
||||
workflowId: command.workflowId,
|
||||
runId: command.runId,
|
||||
limit: command.limit,
|
||||
after: command.after,
|
||||
actor: command.principal.subject,
|
||||
fence: {
|
||||
projectVersion: command.policyFence.projectVersion,
|
||||
bindingVersion: command.policyFence.bindingVersion,
|
||||
},
|
||||
audit: normalizeSecurityAuditRecord({
|
||||
eventId: command.auditEventId,
|
||||
requestId: command.requestId,
|
||||
operationId: 'workflow.step.list',
|
||||
projectId: command.projectId,
|
||||
subject: command.principal.subject,
|
||||
authenticationId: command.principal.authenticationId,
|
||||
outcome: 'allowed',
|
||||
reasons: ['project_policy_allowed'],
|
||||
fence: command.policyFence,
|
||||
occurredAtMs: command.observedAtMs,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
async listRunEvents(
|
||||
command: Readonly<ListClusterPluginPackageWorkflowRunEventsCommand>,
|
||||
) {
|
||||
if (command.policyFence.bindingVersion === null) {
|
||||
throw new ClusterPluginPackageWorkflowUnavailableError();
|
||||
}
|
||||
return runEventLists.listRunEventsAuthorized({
|
||||
projectId: command.projectId,
|
||||
packageName: command.packageName,
|
||||
workflowId: command.workflowId,
|
||||
runId: command.runId,
|
||||
limit: command.limit,
|
||||
afterSequence: command.afterSequence,
|
||||
actor: command.principal.subject,
|
||||
fence: {
|
||||
projectVersion: command.policyFence.projectVersion,
|
||||
bindingVersion: command.policyFence.bindingVersion,
|
||||
},
|
||||
audit: normalizeSecurityAuditRecord({
|
||||
eventId: command.auditEventId,
|
||||
requestId: command.requestId,
|
||||
operationId: 'workflow.event.list',
|
||||
projectId: command.projectId,
|
||||
subject: command.principal.subject,
|
||||
authenticationId: command.principal.authenticationId,
|
||||
outcome: 'allowed',
|
||||
reasons: ['project_policy_allowed'],
|
||||
fence: command.policyFence,
|
||||
occurredAtMs: command.observedAtMs,
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
+707
@@ -0,0 +1,707 @@
|
||||
// Plugin Package Workflow owns its bounded inspect/start/cancel transport adapter.
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import {
|
||||
CLUSTER_RUN_CANCELLATION_SCHEMA,
|
||||
createClusterRunCancellationResponseBody,
|
||||
parseClusterRunCancellationRequestBody,
|
||||
} from '@qinglong/runtime-core/cluster-run-cancellation';
|
||||
import {
|
||||
DEFAULT_PLUGIN_PACKAGE_WORKFLOW_RUN_EVENT_LIST_PAGE_SIZE,
|
||||
DEFAULT_PLUGIN_PACKAGE_WORKFLOW_RUN_LIST_PAGE_SIZE,
|
||||
DEFAULT_PLUGIN_PACKAGE_WORKFLOW_STEP_RUN_LIST_PAGE_SIZE,
|
||||
MAX_PLUGIN_PACKAGE_WORKFLOW_RUN_EVENT_LIST_PAGE_SIZE,
|
||||
MAX_PLUGIN_PACKAGE_WORKFLOW_RUN_LIST_PAGE_SIZE,
|
||||
MAX_PLUGIN_PACKAGE_WORKFLOW_STEP_RUN_LIST_PAGE_SIZE,
|
||||
} from '@qinglong/runtime-core/plugin-package-workflow-administration';
|
||||
import type { ClusterControlAdmissionResponse } from '../../transport/httpSurface';
|
||||
import type { ClusterPluginPackageWorkflowAdministrationCapability } from './pluginPackageWorkflowAdministration';
|
||||
import type {
|
||||
ClusterControlAuthorizedOperationRequest,
|
||||
ClusterControlRouteDefinition,
|
||||
ClusterControlRouteParameters,
|
||||
} from '../../transport/routeRegistry';
|
||||
|
||||
export const CLUSTER_PLUGIN_PACKAGE_WORKFLOW_LIST_RESPONSE_SCHEMA =
|
||||
'qinglong/cluster-plugin-package-workflow-list@v1' as const;
|
||||
export const CLUSTER_PLUGIN_PACKAGE_WORKFLOW_START_REQUEST_SCHEMA =
|
||||
'qinglong/cluster-plugin-package-workflow-start-request@v1' as const;
|
||||
export const CLUSTER_PLUGIN_PACKAGE_WORKFLOW_START_RESPONSE_SCHEMA =
|
||||
'qinglong/cluster-plugin-package-workflow-start-response@v1' as const;
|
||||
|
||||
const UUID_V4 =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
||||
const PACKAGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
||||
const RESOURCE_ID = /^[a-z][a-z0-9-]{0,62}$/;
|
||||
|
||||
function parseRunListQuery(
|
||||
query: Readonly<Record<string, readonly string[]>>,
|
||||
): Readonly<{
|
||||
limit: number;
|
||||
after: Readonly<{ admittedAtMs: number; runId: string }> | null;
|
||||
}> {
|
||||
const limitValues = query.limit;
|
||||
const admittedAtValues = query.after_admitted_at_ms;
|
||||
const runIdValues = query.after_run_id;
|
||||
if (
|
||||
(limitValues !== undefined && limitValues.length !== 1) ||
|
||||
(admittedAtValues !== undefined && admittedAtValues.length !== 1) ||
|
||||
(runIdValues !== undefined && runIdValues.length !== 1) ||
|
||||
(admittedAtValues === undefined) !== (runIdValues === undefined)
|
||||
) {
|
||||
throw new TypeError();
|
||||
}
|
||||
const limit =
|
||||
limitValues === undefined
|
||||
? DEFAULT_PLUGIN_PACKAGE_WORKFLOW_RUN_LIST_PAGE_SIZE
|
||||
: Number(limitValues[0]);
|
||||
if (
|
||||
!Number.isSafeInteger(limit) ||
|
||||
limit < 1 ||
|
||||
limit > MAX_PLUGIN_PACKAGE_WORKFLOW_RUN_LIST_PAGE_SIZE ||
|
||||
(limitValues !== undefined && String(limit) !== limitValues[0])
|
||||
) {
|
||||
throw new TypeError();
|
||||
}
|
||||
if (admittedAtValues === undefined || runIdValues === undefined) {
|
||||
return Object.freeze({ limit, after: null });
|
||||
}
|
||||
const admittedAtMs = Number(admittedAtValues[0]);
|
||||
const runId = runIdValues[0]!;
|
||||
if (
|
||||
!Number.isSafeInteger(admittedAtMs) ||
|
||||
admittedAtMs < 0 ||
|
||||
String(admittedAtMs) !== admittedAtValues[0] ||
|
||||
!UUID_V4.test(runId)
|
||||
) {
|
||||
throw new TypeError();
|
||||
}
|
||||
return Object.freeze({
|
||||
limit,
|
||||
after: Object.freeze({ admittedAtMs, runId }),
|
||||
});
|
||||
}
|
||||
|
||||
function parseStepRunListQuery(
|
||||
query: Readonly<Record<string, readonly string[]>>,
|
||||
): Readonly<{
|
||||
limit: number;
|
||||
after: Readonly<{ stepKey: string; id: string }> | null;
|
||||
}> {
|
||||
const limitValues = query.limit;
|
||||
const stepKeyValues = query.after_step_key;
|
||||
const stepRunIdValues = query.after_step_run_id;
|
||||
if (
|
||||
(limitValues !== undefined && limitValues.length !== 1) ||
|
||||
(stepKeyValues !== undefined && stepKeyValues.length !== 1) ||
|
||||
(stepRunIdValues !== undefined && stepRunIdValues.length !== 1) ||
|
||||
(stepKeyValues === undefined) !== (stepRunIdValues === undefined)
|
||||
) {
|
||||
throw new TypeError();
|
||||
}
|
||||
const limit =
|
||||
limitValues === undefined
|
||||
? DEFAULT_PLUGIN_PACKAGE_WORKFLOW_STEP_RUN_LIST_PAGE_SIZE
|
||||
: Number(limitValues[0]);
|
||||
if (
|
||||
!Number.isSafeInteger(limit) ||
|
||||
limit < 1 ||
|
||||
limit > MAX_PLUGIN_PACKAGE_WORKFLOW_STEP_RUN_LIST_PAGE_SIZE ||
|
||||
(limitValues !== undefined && String(limit) !== limitValues[0])
|
||||
) {
|
||||
throw new TypeError();
|
||||
}
|
||||
if (stepKeyValues === undefined || stepRunIdValues === undefined) {
|
||||
return Object.freeze({ limit, after: null });
|
||||
}
|
||||
const stepKey = stepKeyValues[0]!;
|
||||
const id = stepRunIdValues[0]!;
|
||||
if (!RESOURCE_ID.test(stepKey) || !UUID_V4.test(id)) {
|
||||
throw new TypeError();
|
||||
}
|
||||
return Object.freeze({
|
||||
limit,
|
||||
after: Object.freeze({ stepKey, id }),
|
||||
});
|
||||
}
|
||||
|
||||
function parseRunEventListQuery(
|
||||
query: Readonly<Record<string, readonly string[]>>,
|
||||
): Readonly<{ limit: number; afterSequence: number }> {
|
||||
const limitValues = query.limit;
|
||||
const afterValues = query.after_sequence;
|
||||
if (
|
||||
(limitValues !== undefined && limitValues.length !== 1) ||
|
||||
(afterValues !== undefined && afterValues.length !== 1)
|
||||
) {
|
||||
throw new TypeError();
|
||||
}
|
||||
const limit =
|
||||
limitValues === undefined
|
||||
? DEFAULT_PLUGIN_PACKAGE_WORKFLOW_RUN_EVENT_LIST_PAGE_SIZE
|
||||
: Number(limitValues[0]);
|
||||
const afterSequence = afterValues === undefined ? 0 : Number(afterValues[0]);
|
||||
if (
|
||||
!Number.isSafeInteger(limit) ||
|
||||
limit < 1 ||
|
||||
limit > MAX_PLUGIN_PACKAGE_WORKFLOW_RUN_EVENT_LIST_PAGE_SIZE ||
|
||||
(limitValues !== undefined && String(limit) !== limitValues[0]) ||
|
||||
!Number.isSafeInteger(afterSequence) ||
|
||||
afterSequence < 0 ||
|
||||
(afterValues !== undefined && String(afterSequence) !== afterValues[0])
|
||||
) {
|
||||
throw new TypeError();
|
||||
}
|
||||
return Object.freeze({ limit, afterSequence });
|
||||
}
|
||||
|
||||
function response(
|
||||
statusCode: number,
|
||||
body: Readonly<Record<string, unknown>>,
|
||||
): ClusterControlAdmissionResponse {
|
||||
return Object.freeze({ statusCode, body: Object.freeze(body) });
|
||||
}
|
||||
|
||||
function parseBody(value: unknown): Readonly<{
|
||||
planId: string;
|
||||
runId: string;
|
||||
stepRunIds: Readonly<Record<string, string>>;
|
||||
}> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw new TypeError();
|
||||
}
|
||||
const body = value as Record<string, unknown>;
|
||||
if (
|
||||
Object.keys(body).sort().join(',') !== 'planId,runId,schema,stepRunIds' ||
|
||||
body.schema !== CLUSTER_PLUGIN_PACKAGE_WORKFLOW_START_REQUEST_SCHEMA ||
|
||||
typeof body.planId !== 'string' ||
|
||||
!UUID_V4.test(body.planId) ||
|
||||
typeof body.runId !== 'string' ||
|
||||
!UUID_V4.test(body.runId) ||
|
||||
!body.stepRunIds ||
|
||||
typeof body.stepRunIds !== 'object' ||
|
||||
Array.isArray(body.stepRunIds) ||
|
||||
Object.getPrototypeOf(body.stepRunIds) !== Object.prototype
|
||||
) {
|
||||
throw new TypeError();
|
||||
}
|
||||
const entries = Object.entries(body.stepRunIds as Record<string, unknown>);
|
||||
if (
|
||||
entries.length < 1 ||
|
||||
entries.length > 128 ||
|
||||
entries.some(
|
||||
([key, id]) =>
|
||||
!RESOURCE_ID.test(key) || typeof id !== 'string' || !UUID_V4.test(id),
|
||||
) ||
|
||||
new Set(entries.map(([, id]) => id)).size !== entries.length
|
||||
) {
|
||||
throw new TypeError();
|
||||
}
|
||||
return Object.freeze({
|
||||
planId: body.planId,
|
||||
runId: body.runId,
|
||||
stepRunIds: Object.freeze(
|
||||
Object.fromEntries(entries) as Record<string, string>,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
function errorResponse(error: unknown): ClusterControlAdmissionResponse {
|
||||
const code =
|
||||
error && typeof error === 'object' && 'code' in error
|
||||
? (error as { code?: unknown }).code
|
||||
: null;
|
||||
if (code === 'CLUSTER_PLUGIN_PACKAGE_WORKFLOW_NOT_FOUND') {
|
||||
return response(404, { code: 'workflow_not_found' });
|
||||
}
|
||||
if (
|
||||
code === 'CLUSTER_PLUGIN_PACKAGE_WORKFLOW_CONFLICT' ||
|
||||
code === 'PLUGIN_PACKAGE_WORKFLOW_ADMINISTRATION_MUTATION_CONFLICT' ||
|
||||
code === 'PLUGIN_PACKAGE_WORKFLOW_ADMISSION_CONFLICT'
|
||||
) {
|
||||
return response(409, { code: 'workflow_start_conflict' });
|
||||
}
|
||||
if (
|
||||
code ===
|
||||
'PLUGIN_PACKAGE_WORKFLOW_ADMINISTRATION_AUTHORIZATION_FENCE_CONFLICT'
|
||||
) {
|
||||
return response(409, { code: 'authorization_fence_changed' });
|
||||
}
|
||||
if (code === 'CLUSTER_RUN_CANCELLATION_NOT_FOUND') {
|
||||
return response(404, { code: 'workflow_run_not_found' });
|
||||
}
|
||||
if (code === 'CLUSTER_RUN_CANCELLATION_FENCE_REJECTED') {
|
||||
const candidateReason =
|
||||
error && typeof error === 'object' && 'reason' in error
|
||||
? (error as { reason?: unknown }).reason
|
||||
: null;
|
||||
const reason =
|
||||
candidateReason === 'authorization_changed' ||
|
||||
candidateReason === 'project_mismatch' ||
|
||||
candidateReason === 'state_mismatch'
|
||||
? candidateReason
|
||||
: 'state_mismatch';
|
||||
return response(409, {
|
||||
code: 'workflow_cancellation_fence_rejected',
|
||||
reason,
|
||||
});
|
||||
}
|
||||
return response(503, { code: 'workflow_administration_unavailable' });
|
||||
}
|
||||
|
||||
function runInspectionErrorResponse(
|
||||
error: unknown,
|
||||
): ClusterControlAdmissionResponse {
|
||||
const code =
|
||||
error && typeof error === 'object' && 'code' in error
|
||||
? (error as { code?: unknown }).code
|
||||
: null;
|
||||
if (
|
||||
code ===
|
||||
'PLUGIN_PACKAGE_WORKFLOW_ADMINISTRATION_AUTHORIZATION_FENCE_CONFLICT'
|
||||
) {
|
||||
return response(409, { code: 'authorization_fence_changed' });
|
||||
}
|
||||
return response(503, { code: 'workflow_run_query_unavailable' });
|
||||
}
|
||||
|
||||
function runListErrorResponse(error: unknown): ClusterControlAdmissionResponse {
|
||||
const code =
|
||||
error && typeof error === 'object' && 'code' in error
|
||||
? (error as { code?: unknown }).code
|
||||
: null;
|
||||
if (
|
||||
code ===
|
||||
'PLUGIN_PACKAGE_WORKFLOW_ADMINISTRATION_AUTHORIZATION_FENCE_CONFLICT'
|
||||
) {
|
||||
return response(409, { code: 'authorization_fence_changed' });
|
||||
}
|
||||
return response(503, { code: 'workflow_run_list_unavailable' });
|
||||
}
|
||||
|
||||
function stepRunListErrorResponse(
|
||||
error: unknown,
|
||||
): ClusterControlAdmissionResponse {
|
||||
const code =
|
||||
error && typeof error === 'object' && 'code' in error
|
||||
? (error as { code?: unknown }).code
|
||||
: null;
|
||||
if (
|
||||
code ===
|
||||
'PLUGIN_PACKAGE_WORKFLOW_ADMINISTRATION_AUTHORIZATION_FENCE_CONFLICT'
|
||||
) {
|
||||
return response(409, { code: 'authorization_fence_changed' });
|
||||
}
|
||||
return response(503, { code: 'workflow_step_run_query_unavailable' });
|
||||
}
|
||||
|
||||
function runEventListErrorResponse(
|
||||
error: unknown,
|
||||
): ClusterControlAdmissionResponse {
|
||||
const code =
|
||||
error && typeof error === 'object' && 'code' in error
|
||||
? (error as { code?: unknown }).code
|
||||
: null;
|
||||
if (
|
||||
code ===
|
||||
'PLUGIN_PACKAGE_WORKFLOW_ADMINISTRATION_AUTHORIZATION_FENCE_CONFLICT'
|
||||
) {
|
||||
return response(409, { code: 'authorization_fence_changed' });
|
||||
}
|
||||
return response(503, { code: 'workflow_run_event_query_unavailable' });
|
||||
}
|
||||
|
||||
export function createClusterControlPluginPackageWorkflowRoutes(
|
||||
capability: ClusterPluginPackageWorkflowAdministrationCapability,
|
||||
now: () => number = Date.now,
|
||||
createEventId: () => string = randomUUID,
|
||||
): readonly Readonly<ClusterControlRouteDefinition>[] {
|
||||
if (
|
||||
!capability ||
|
||||
typeof capability.inspect !== 'function' ||
|
||||
typeof capability.inspectRun !== 'function' ||
|
||||
typeof capability.listRuns !== 'function' ||
|
||||
typeof capability.listStepRuns !== 'function' ||
|
||||
typeof capability.listRunEvents !== 'function' ||
|
||||
typeof capability.start !== 'function' ||
|
||||
typeof capability.cancel !== 'function' ||
|
||||
typeof now !== 'function' ||
|
||||
typeof createEventId !== 'function'
|
||||
) {
|
||||
throw new TypeError('Cluster-control Workflow capability is invalid');
|
||||
}
|
||||
const common = {
|
||||
projectParameter: 'projectId' as const,
|
||||
};
|
||||
return Object.freeze([
|
||||
Object.freeze({
|
||||
...common,
|
||||
method: 'GET' as const,
|
||||
path: '/api/v3/projects/{projectId}/packages/{packageName}/workflows',
|
||||
operationId: 'workflow.read',
|
||||
permission: 'run.read',
|
||||
async handle(
|
||||
authorized: ClusterControlAuthorizedOperationRequest,
|
||||
parameters: ClusterControlRouteParameters,
|
||||
) {
|
||||
if (
|
||||
authorized.projectId === null ||
|
||||
typeof parameters.packageName !== 'string' ||
|
||||
!PACKAGE_NAME.test(parameters.packageName)
|
||||
) {
|
||||
return response(503, { code: 'workflow_administration_unavailable' });
|
||||
}
|
||||
try {
|
||||
const result = await capability.inspect(
|
||||
authorized.projectId,
|
||||
parameters.packageName,
|
||||
);
|
||||
return response(200, {
|
||||
schema: CLUSTER_PLUGIN_PACKAGE_WORKFLOW_LIST_RESPONSE_SCHEMA,
|
||||
...result,
|
||||
});
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
},
|
||||
}),
|
||||
Object.freeze({
|
||||
...common,
|
||||
method: 'GET' as const,
|
||||
path: '/api/v3/projects/{projectId}/packages/{packageName}/workflows/{workflowId}/runs',
|
||||
operationId: 'workflow.run.list',
|
||||
permission: 'run.read',
|
||||
allowedQuery: Object.freeze([
|
||||
'after_admitted_at_ms',
|
||||
'after_run_id',
|
||||
'limit',
|
||||
]),
|
||||
async handle(
|
||||
authorized: ClusterControlAuthorizedOperationRequest,
|
||||
parameters: ClusterControlRouteParameters,
|
||||
) {
|
||||
if (authorized.request.body !== null) {
|
||||
return response(400, { code: 'invalid_request_body' });
|
||||
}
|
||||
let page;
|
||||
try {
|
||||
page = parseRunListQuery(authorized.request.query);
|
||||
} catch {
|
||||
return response(400, { code: 'invalid_workflow_run_query' });
|
||||
}
|
||||
const observedAtMs = now();
|
||||
if (
|
||||
authorized.projectId === null ||
|
||||
typeof parameters.packageName !== 'string' ||
|
||||
!PACKAGE_NAME.test(parameters.packageName) ||
|
||||
typeof parameters.workflowId !== 'string' ||
|
||||
!RESOURCE_ID.test(parameters.workflowId) ||
|
||||
!authorized.policyFence ||
|
||||
authorized.policyFence.bindingVersion === null ||
|
||||
!Number.isSafeInteger(observedAtMs) ||
|
||||
observedAtMs < 0
|
||||
) {
|
||||
return response(503, { code: 'workflow_run_list_unavailable' });
|
||||
}
|
||||
try {
|
||||
const result = await capability.listRuns({
|
||||
projectId: authorized.projectId,
|
||||
packageName: parameters.packageName,
|
||||
workflowId: parameters.workflowId,
|
||||
limit: page.limit,
|
||||
after: page.after,
|
||||
requestId: authorized.request.requestId,
|
||||
auditEventId: createEventId(),
|
||||
principal: authorized.principal,
|
||||
policyFence: authorized.policyFence,
|
||||
observedAtMs,
|
||||
});
|
||||
return response(200, { ...result });
|
||||
} catch (error) {
|
||||
return runListErrorResponse(error);
|
||||
}
|
||||
},
|
||||
}),
|
||||
Object.freeze({
|
||||
...common,
|
||||
method: 'GET' as const,
|
||||
path: '/api/v3/projects/{projectId}/packages/{packageName}/workflows/{workflowId}/runs/{runId}',
|
||||
operationId: 'workflow.run.read',
|
||||
permission: 'run.read',
|
||||
async handle(
|
||||
authorized: ClusterControlAuthorizedOperationRequest,
|
||||
parameters: ClusterControlRouteParameters,
|
||||
) {
|
||||
const observedAtMs = now();
|
||||
if (authorized.request.body !== null) {
|
||||
return response(400, { code: 'invalid_request_body' });
|
||||
}
|
||||
if (
|
||||
authorized.projectId === null ||
|
||||
typeof parameters.packageName !== 'string' ||
|
||||
!PACKAGE_NAME.test(parameters.packageName) ||
|
||||
typeof parameters.workflowId !== 'string' ||
|
||||
!RESOURCE_ID.test(parameters.workflowId) ||
|
||||
typeof parameters.runId !== 'string' ||
|
||||
!UUID_V4.test(parameters.runId) ||
|
||||
!authorized.policyFence ||
|
||||
authorized.policyFence.bindingVersion === null ||
|
||||
!Number.isSafeInteger(observedAtMs) ||
|
||||
observedAtMs < 0
|
||||
) {
|
||||
return response(503, { code: 'workflow_run_query_unavailable' });
|
||||
}
|
||||
try {
|
||||
const result = await capability.inspectRun({
|
||||
projectId: authorized.projectId,
|
||||
packageName: parameters.packageName,
|
||||
workflowId: parameters.workflowId,
|
||||
runId: parameters.runId,
|
||||
requestId: authorized.request.requestId,
|
||||
auditEventId: createEventId(),
|
||||
principal: authorized.principal,
|
||||
policyFence: authorized.policyFence,
|
||||
observedAtMs,
|
||||
});
|
||||
return result.found
|
||||
? response(200, { ...result })
|
||||
: response(404, { code: 'workflow_run_not_found' });
|
||||
} catch (error) {
|
||||
return runInspectionErrorResponse(error);
|
||||
}
|
||||
},
|
||||
}),
|
||||
Object.freeze({
|
||||
...common,
|
||||
method: 'GET' as const,
|
||||
path: '/api/v3/projects/{projectId}/packages/{packageName}/workflows/{workflowId}/runs/{runId}/steps',
|
||||
operationId: 'workflow.step.list',
|
||||
permission: 'run.read',
|
||||
allowedQuery: Object.freeze([
|
||||
'after_step_key',
|
||||
'after_step_run_id',
|
||||
'limit',
|
||||
]),
|
||||
async handle(
|
||||
authorized: ClusterControlAuthorizedOperationRequest,
|
||||
parameters: ClusterControlRouteParameters,
|
||||
) {
|
||||
if (authorized.request.body !== null) {
|
||||
return response(400, { code: 'invalid_request_body' });
|
||||
}
|
||||
let page;
|
||||
try {
|
||||
page = parseStepRunListQuery(authorized.request.query);
|
||||
} catch {
|
||||
return response(400, { code: 'invalid_step_run_query' });
|
||||
}
|
||||
const observedAtMs = now();
|
||||
if (
|
||||
authorized.projectId === null ||
|
||||
typeof parameters.packageName !== 'string' ||
|
||||
!PACKAGE_NAME.test(parameters.packageName) ||
|
||||
typeof parameters.workflowId !== 'string' ||
|
||||
!RESOURCE_ID.test(parameters.workflowId) ||
|
||||
typeof parameters.runId !== 'string' ||
|
||||
!UUID_V4.test(parameters.runId) ||
|
||||
!authorized.policyFence ||
|
||||
authorized.policyFence.bindingVersion === null ||
|
||||
!Number.isSafeInteger(observedAtMs) ||
|
||||
observedAtMs < 0
|
||||
) {
|
||||
return response(503, { code: 'workflow_step_run_query_unavailable' });
|
||||
}
|
||||
try {
|
||||
const result = await capability.listStepRuns({
|
||||
projectId: authorized.projectId,
|
||||
packageName: parameters.packageName,
|
||||
workflowId: parameters.workflowId,
|
||||
runId: parameters.runId,
|
||||
limit: page.limit,
|
||||
after: page.after,
|
||||
requestId: authorized.request.requestId,
|
||||
auditEventId: createEventId(),
|
||||
principal: authorized.principal,
|
||||
policyFence: authorized.policyFence,
|
||||
observedAtMs,
|
||||
});
|
||||
return result.found
|
||||
? response(200, { ...result })
|
||||
: response(404, { code: 'workflow_run_not_found' });
|
||||
} catch (error) {
|
||||
return stepRunListErrorResponse(error);
|
||||
}
|
||||
},
|
||||
}),
|
||||
Object.freeze({
|
||||
...common,
|
||||
method: 'GET' as const,
|
||||
path: '/api/v3/projects/{projectId}/packages/{packageName}/workflows/{workflowId}/runs/{runId}/events',
|
||||
operationId: 'workflow.event.list',
|
||||
permission: 'run.read',
|
||||
allowedQuery: Object.freeze(['after_sequence', 'limit']),
|
||||
async handle(
|
||||
authorized: ClusterControlAuthorizedOperationRequest,
|
||||
parameters: ClusterControlRouteParameters,
|
||||
) {
|
||||
if (authorized.request.body !== null) {
|
||||
return response(400, { code: 'invalid_request_body' });
|
||||
}
|
||||
let page;
|
||||
try {
|
||||
page = parseRunEventListQuery(authorized.request.query);
|
||||
} catch {
|
||||
return response(400, { code: 'invalid_run_event_query' });
|
||||
}
|
||||
const observedAtMs = now();
|
||||
if (
|
||||
authorized.projectId === null ||
|
||||
typeof parameters.packageName !== 'string' ||
|
||||
!PACKAGE_NAME.test(parameters.packageName) ||
|
||||
typeof parameters.workflowId !== 'string' ||
|
||||
!RESOURCE_ID.test(parameters.workflowId) ||
|
||||
typeof parameters.runId !== 'string' ||
|
||||
!UUID_V4.test(parameters.runId) ||
|
||||
!authorized.policyFence ||
|
||||
authorized.policyFence.bindingVersion === null ||
|
||||
!Number.isSafeInteger(observedAtMs) ||
|
||||
observedAtMs < 0
|
||||
) {
|
||||
return response(503, {
|
||||
code: 'workflow_run_event_query_unavailable',
|
||||
});
|
||||
}
|
||||
try {
|
||||
const result = await capability.listRunEvents({
|
||||
projectId: authorized.projectId,
|
||||
packageName: parameters.packageName,
|
||||
workflowId: parameters.workflowId,
|
||||
runId: parameters.runId,
|
||||
limit: page.limit,
|
||||
afterSequence: page.afterSequence,
|
||||
requestId: authorized.request.requestId,
|
||||
auditEventId: createEventId(),
|
||||
principal: authorized.principal,
|
||||
policyFence: authorized.policyFence,
|
||||
observedAtMs,
|
||||
});
|
||||
return result.found
|
||||
? response(200, { ...result })
|
||||
: response(404, { code: 'workflow_run_not_found' });
|
||||
} catch (error) {
|
||||
return runEventListErrorResponse(error);
|
||||
}
|
||||
},
|
||||
}),
|
||||
Object.freeze({
|
||||
...common,
|
||||
method: 'POST' as const,
|
||||
path: '/api/v3/projects/{projectId}/packages/{packageName}/workflows/{workflowId}/runs',
|
||||
operationId: 'workflow.start',
|
||||
permission: 'run.start',
|
||||
async handle(
|
||||
authorized: ClusterControlAuthorizedOperationRequest,
|
||||
parameters: ClusterControlRouteParameters,
|
||||
) {
|
||||
let body;
|
||||
try {
|
||||
body = parseBody(authorized.request.body);
|
||||
} catch {
|
||||
return response(400, { code: 'invalid_workflow_start_request' });
|
||||
}
|
||||
const plannedAtMs = now();
|
||||
if (
|
||||
authorized.projectId === null ||
|
||||
typeof parameters.packageName !== 'string' ||
|
||||
!PACKAGE_NAME.test(parameters.packageName) ||
|
||||
typeof parameters.workflowId !== 'string' ||
|
||||
!RESOURCE_ID.test(parameters.workflowId) ||
|
||||
!authorized.policyFence ||
|
||||
authorized.policyFence.bindingVersion === null ||
|
||||
!Number.isSafeInteger(plannedAtMs) ||
|
||||
plannedAtMs < 0
|
||||
) {
|
||||
return response(503, { code: 'workflow_administration_unavailable' });
|
||||
}
|
||||
try {
|
||||
const result = await capability.start({
|
||||
projectId: authorized.projectId,
|
||||
packageName: parameters.packageName,
|
||||
workflowId: parameters.workflowId,
|
||||
planId: body.planId,
|
||||
runId: body.runId,
|
||||
stepRunIds: body.stepRunIds,
|
||||
principal: authorized.principal,
|
||||
policyFence: authorized.policyFence,
|
||||
plannedAtMs,
|
||||
});
|
||||
return response(result.status === 'created' ? 201 : 200, {
|
||||
schema: CLUSTER_PLUGIN_PACKAGE_WORKFLOW_START_RESPONSE_SCHEMA,
|
||||
status: result.status,
|
||||
replayed: result.status === 'existing',
|
||||
planId: result.plan.planId,
|
||||
runId: result.plan.runId,
|
||||
receiptDigest: result.receipt.receiptDigest,
|
||||
});
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
},
|
||||
}),
|
||||
Object.freeze({
|
||||
...common,
|
||||
method: 'POST' as const,
|
||||
path: '/api/v3/projects/{projectId}/packages/{packageName}/workflows/{workflowId}/runs/{runId}/cancellation',
|
||||
operationId: 'workflow.cancel',
|
||||
permission: 'run.stop',
|
||||
async handle(
|
||||
authorized: ClusterControlAuthorizedOperationRequest,
|
||||
parameters: ClusterControlRouteParameters,
|
||||
) {
|
||||
let body;
|
||||
try {
|
||||
body = parseClusterRunCancellationRequestBody(
|
||||
authorized.request.body,
|
||||
);
|
||||
} catch {
|
||||
return response(400, {
|
||||
code: 'invalid_workflow_cancellation_request',
|
||||
schema: CLUSTER_RUN_CANCELLATION_SCHEMA,
|
||||
});
|
||||
}
|
||||
if (
|
||||
authorized.projectId === null ||
|
||||
typeof parameters.packageName !== 'string' ||
|
||||
!PACKAGE_NAME.test(parameters.packageName) ||
|
||||
typeof parameters.workflowId !== 'string' ||
|
||||
!RESOURCE_ID.test(parameters.workflowId) ||
|
||||
typeof parameters.runId !== 'string' ||
|
||||
!UUID_V4.test(parameters.runId) ||
|
||||
!authorized.policyFence ||
|
||||
authorized.policyFence.bindingVersion === null
|
||||
) {
|
||||
return response(503, {
|
||||
code: 'workflow_administration_unavailable',
|
||||
});
|
||||
}
|
||||
try {
|
||||
const result = await capability.cancel({
|
||||
projectId: authorized.projectId,
|
||||
packageName: parameters.packageName,
|
||||
workflowId: parameters.workflowId,
|
||||
runId: parameters.runId,
|
||||
mutationId: body.mutationId,
|
||||
eventId: createEventId(),
|
||||
principal: authorized.principal,
|
||||
policyFence: authorized.policyFence,
|
||||
});
|
||||
return response(
|
||||
result.status === 'accepted' ? 202 : 200,
|
||||
createClusterRunCancellationResponseBody(result),
|
||||
);
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
Reference in New Issue
Block a user