mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): add explicit cluster observation console
This commit is contained in:
@@ -75,6 +75,24 @@ export interface ClusterCopilotClientReadiness {
|
||||
readonly ready: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Package-internal read transport shared by the Copilot client and the
|
||||
* loopback operator console. The caller owns the reviewed path vocabulary;
|
||||
* this boundary still rejects non-Project, mutation and cross-origin targets.
|
||||
*/
|
||||
export interface ClusterProjectApiReadExecution {
|
||||
readonly configFile: string;
|
||||
readonly credentialFile: string;
|
||||
readonly path: string;
|
||||
readonly requestId: string;
|
||||
}
|
||||
|
||||
export interface ClusterProjectApiReadResult {
|
||||
readonly schemaVersion: 1;
|
||||
readonly requestId: string;
|
||||
readonly result: Readonly<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
interface PreparedClusterCopilotClientConfiguration {
|
||||
readonly endpoint: URL;
|
||||
readonly servername: string;
|
||||
@@ -134,6 +152,163 @@ const DNS_NAME =
|
||||
const API_CREDENTIAL =
|
||||
/^ql3c_[A-Za-z0-9][A-Za-z0-9._:-]{0,63}_[A-Za-z0-9_-]{43}$/;
|
||||
const RESPONSE_CODE = /^[a-z][a-z0-9_]{0,127}$/;
|
||||
const TRANSPORT_REQUEST_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
const PROJECT_SEGMENT = '[A-Za-z0-9][A-Za-z0-9._:-]{0,127}';
|
||||
const PACKAGE_SEGMENT = '[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?';
|
||||
const WORKFLOW_SEGMENT = '[a-z][a-z0-9-]{0,62}';
|
||||
const UUID_SEGMENT =
|
||||
'[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}';
|
||||
const POSITIVE_LIMIT = '(?:[1-9]|[1-5][0-9]|6[0-4])';
|
||||
const NON_NEGATIVE_INTEGER = '(?:0|[1-9][0-9]{0,15})';
|
||||
const projectRoot = '/api/v3/projects/' + PROJECT_SEGMENT;
|
||||
const PROJECT_READ_PATHS = Object.freeze([
|
||||
new RegExp('^' + projectRoot + '/runs\\?limit=' + POSITIVE_LIMIT + '$'),
|
||||
new RegExp(
|
||||
'^' +
|
||||
projectRoot +
|
||||
'/runs\\?after_created_at_ms=' +
|
||||
NON_NEGATIVE_INTEGER +
|
||||
'&after_run_id=' +
|
||||
PROJECT_SEGMENT +
|
||||
'&limit=' +
|
||||
POSITIVE_LIMIT +
|
||||
'$',
|
||||
),
|
||||
new RegExp('^' + projectRoot + '/runs/' + PROJECT_SEGMENT + '$'),
|
||||
new RegExp(
|
||||
'^' +
|
||||
projectRoot +
|
||||
'/runs/' +
|
||||
PROJECT_SEGMENT +
|
||||
'/events\\?after_sequence=' +
|
||||
NON_NEGATIVE_INTEGER +
|
||||
'&limit=' +
|
||||
POSITIVE_LIMIT +
|
||||
'$',
|
||||
),
|
||||
new RegExp(
|
||||
'^' +
|
||||
projectRoot +
|
||||
'/runs/' +
|
||||
PROJECT_SEGMENT +
|
||||
'/steps\\?limit=' +
|
||||
POSITIVE_LIMIT +
|
||||
'$',
|
||||
),
|
||||
new RegExp(
|
||||
'^' +
|
||||
projectRoot +
|
||||
'/runs/' +
|
||||
PROJECT_SEGMENT +
|
||||
'/steps\\?after_step_key=' +
|
||||
PROJECT_SEGMENT +
|
||||
'&after_step_run_id=' +
|
||||
PROJECT_SEGMENT +
|
||||
'&limit=' +
|
||||
POSITIVE_LIMIT +
|
||||
'$',
|
||||
),
|
||||
new RegExp('^' + projectRoot + '/tasks\\?limit=' + POSITIVE_LIMIT + '$'),
|
||||
new RegExp(
|
||||
'^' +
|
||||
projectRoot +
|
||||
'/tasks\\?after_task_id=' +
|
||||
PROJECT_SEGMENT +
|
||||
'&limit=' +
|
||||
POSITIVE_LIMIT +
|
||||
'$',
|
||||
),
|
||||
new RegExp('^' + projectRoot + '/tasks/' + PROJECT_SEGMENT + '$'),
|
||||
new RegExp(
|
||||
'^' + projectRoot + '/packages/' + PACKAGE_SEGMENT + '/workflows$',
|
||||
),
|
||||
new RegExp(
|
||||
'^' +
|
||||
projectRoot +
|
||||
'/packages/' +
|
||||
PACKAGE_SEGMENT +
|
||||
'/workflows/' +
|
||||
WORKFLOW_SEGMENT +
|
||||
'/runs\\?limit=' +
|
||||
POSITIVE_LIMIT +
|
||||
'$',
|
||||
),
|
||||
new RegExp(
|
||||
'^' +
|
||||
projectRoot +
|
||||
'/packages/' +
|
||||
PACKAGE_SEGMENT +
|
||||
'/workflows/' +
|
||||
WORKFLOW_SEGMENT +
|
||||
'/runs\\?after_admitted_at_ms=' +
|
||||
NON_NEGATIVE_INTEGER +
|
||||
'&after_run_id=' +
|
||||
UUID_SEGMENT +
|
||||
'&limit=' +
|
||||
POSITIVE_LIMIT +
|
||||
'$',
|
||||
),
|
||||
new RegExp(
|
||||
'^' +
|
||||
projectRoot +
|
||||
'/packages/' +
|
||||
PACKAGE_SEGMENT +
|
||||
'/workflows/' +
|
||||
WORKFLOW_SEGMENT +
|
||||
'/runs/' +
|
||||
UUID_SEGMENT +
|
||||
'$',
|
||||
),
|
||||
new RegExp(
|
||||
'^' +
|
||||
projectRoot +
|
||||
'/packages/' +
|
||||
PACKAGE_SEGMENT +
|
||||
'/workflows/' +
|
||||
WORKFLOW_SEGMENT +
|
||||
'/runs/' +
|
||||
UUID_SEGMENT +
|
||||
'/events\\?after_sequence=' +
|
||||
NON_NEGATIVE_INTEGER +
|
||||
'&limit=' +
|
||||
POSITIVE_LIMIT +
|
||||
'$',
|
||||
),
|
||||
new RegExp(
|
||||
'^' +
|
||||
projectRoot +
|
||||
'/packages/' +
|
||||
PACKAGE_SEGMENT +
|
||||
'/workflows/' +
|
||||
WORKFLOW_SEGMENT +
|
||||
'/runs/' +
|
||||
UUID_SEGMENT +
|
||||
'/steps\\?limit=' +
|
||||
POSITIVE_LIMIT +
|
||||
'$',
|
||||
),
|
||||
new RegExp(
|
||||
'^' +
|
||||
projectRoot +
|
||||
'/packages/' +
|
||||
PACKAGE_SEGMENT +
|
||||
'/workflows/' +
|
||||
WORKFLOW_SEGMENT +
|
||||
'/runs/' +
|
||||
UUID_SEGMENT +
|
||||
'/steps\\?after_step_key=' +
|
||||
WORKFLOW_SEGMENT +
|
||||
'&after_step_run_id=' +
|
||||
UUID_SEGMENT +
|
||||
'&limit=' +
|
||||
POSITIVE_LIMIT +
|
||||
'$',
|
||||
),
|
||||
]);
|
||||
|
||||
function projectReadPathValid(value: string): boolean {
|
||||
return PROJECT_READ_PATHS.some((pattern) => pattern.test(value));
|
||||
}
|
||||
|
||||
function configurationFailure(): never {
|
||||
throw new ClusterCopilotClientConfigurationError();
|
||||
@@ -413,10 +588,7 @@ function requestJson(
|
||||
} catch (cause) {
|
||||
throw new ClusterCopilotClientRequestError({ cause });
|
||||
}
|
||||
finish(
|
||||
undefined,
|
||||
Object.freeze({ ...provisional, body }),
|
||||
);
|
||||
finish(undefined, Object.freeze({ ...provisional, body }));
|
||||
} catch (error) {
|
||||
finish(
|
||||
error instanceof ClusterCopilotClientRequestError
|
||||
@@ -447,10 +619,7 @@ function requestJson(
|
||||
});
|
||||
}
|
||||
|
||||
function responseRequestId(
|
||||
response: JsonResponse,
|
||||
expected: string,
|
||||
): string {
|
||||
function responseRequestId(response: JsonResponse, expected: string): string {
|
||||
const value = response.headers['x-request-id'];
|
||||
if (
|
||||
rawHeaderCount(response.rawHeaders, 'x-request-id') !== 1 ||
|
||||
@@ -462,7 +631,9 @@ function responseRequestId(
|
||||
return value;
|
||||
}
|
||||
|
||||
function retryAfterSeconds(value: string | string[] | undefined): number | null {
|
||||
function retryAfterSeconds(
|
||||
value: string | string[] | undefined,
|
||||
): number | null {
|
||||
if (typeof value !== 'string' || !/^[1-9][0-9]{0,3}$/.test(value)) {
|
||||
return null;
|
||||
}
|
||||
@@ -480,7 +651,9 @@ function remoteCode(value: unknown): string {
|
||||
keys.length < 1 ||
|
||||
keys.length > 3 ||
|
||||
keys[0] !== 'code' ||
|
||||
keys.some((key) => key !== 'code' && key !== 'reason' && key !== 'schema') ||
|
||||
keys.some(
|
||||
(key) => key !== 'code' && key !== 'reason' && key !== 'schema',
|
||||
) ||
|
||||
typeof record.code !== 'string' ||
|
||||
!RESPONSE_CODE.test(record.code)
|
||||
) {
|
||||
@@ -521,9 +694,7 @@ function readCredentialBytes(credentialFile: string): Buffer {
|
||||
return bytes;
|
||||
} catch (error) {
|
||||
bytes?.fill(0);
|
||||
if (
|
||||
error instanceof ClusterCopilotClientConfigurationError
|
||||
) {
|
||||
if (error instanceof ClusterCopilotClientConfigurationError) {
|
||||
throw error;
|
||||
}
|
||||
throw new ClusterCopilotClientConfigurationError();
|
||||
@@ -552,8 +723,7 @@ export async function probeClusterCopilotClientReadiness(
|
||||
);
|
||||
const status = readinessStatus(response.body);
|
||||
const ready = response.statusCode === 200 && status === 'ready';
|
||||
const notReady =
|
||||
response.statusCode === 503 && status === 'not_ready';
|
||||
const notReady = response.statusCode === 503 && status === 'not_ready';
|
||||
if (!ready && !notReady) throw new ClusterCopilotClientRequestError();
|
||||
return Object.freeze({ schemaVersion: 1, transport: 'https', ready });
|
||||
} catch (error) {
|
||||
@@ -594,10 +764,7 @@ async function executeNormalizedClusterCopilotCommand(
|
||||
);
|
||||
if (request.body !== null) {
|
||||
bodyBytes = Buffer.from(JSON.stringify(request.body), 'utf8');
|
||||
if (
|
||||
bodyBytes.length < 2 ||
|
||||
bodyBytes.length > MAXIMUM_COMMAND_BYTES
|
||||
) {
|
||||
if (bodyBytes.length < 2 || bodyBytes.length > MAXIMUM_COMMAND_BYTES) {
|
||||
return configurationFailure();
|
||||
}
|
||||
}
|
||||
@@ -661,11 +828,7 @@ export async function executeClusterCopilotCommand(
|
||||
options?: ClusterCopilotClientOptions,
|
||||
): Promise<Readonly<ClusterCopilotClientResult>> {
|
||||
const normalizedOptions = validateOptions(options);
|
||||
const record = exact(execution, [
|
||||
'command',
|
||||
'configFile',
|
||||
'credentialFile',
|
||||
]);
|
||||
const record = exact(execution, ['command', 'configFile', 'credentialFile']);
|
||||
const command = normalizeClusterCopilotClientCommand(record.command);
|
||||
return executeNormalizedClusterCopilotCommand(
|
||||
record.configFile as string,
|
||||
@@ -680,11 +843,7 @@ export async function executeClusterCopilotClient(
|
||||
options?: ClusterCopilotClientOptions,
|
||||
): Promise<Readonly<ClusterCopilotClientResult>> {
|
||||
const normalizedOptions = validateOptions(options);
|
||||
const record = exact(paths, [
|
||||
'commandFile',
|
||||
'configFile',
|
||||
'credentialFile',
|
||||
]);
|
||||
const record = exact(paths, ['commandFile', 'configFile', 'credentialFile']);
|
||||
let commandBytes: Buffer | undefined;
|
||||
try {
|
||||
commandBytes = readCanonicalFile(
|
||||
@@ -712,3 +871,89 @@ export async function executeClusterCopilotClient(
|
||||
commandBytes?.fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeClusterProjectApiRead(
|
||||
execution: ClusterProjectApiReadExecution,
|
||||
options?: ClusterCopilotClientOptions,
|
||||
): Promise<Readonly<ClusterProjectApiReadResult>> {
|
||||
const normalizedOptions = validateOptions(options);
|
||||
const record = exact(execution, [
|
||||
'configFile',
|
||||
'credentialFile',
|
||||
'path',
|
||||
'requestId',
|
||||
]);
|
||||
if (
|
||||
typeof record.path !== 'string' ||
|
||||
record.path.length > 2_048 ||
|
||||
!projectReadPathValid(record.path) ||
|
||||
record.path.includes('..') ||
|
||||
record.path.includes('//') ||
|
||||
typeof record.requestId !== 'string' ||
|
||||
!TRANSPORT_REQUEST_ID.test(record.requestId)
|
||||
) {
|
||||
throw new ClusterCopilotClientRequestError();
|
||||
}
|
||||
let credentialBytes: Buffer | undefined;
|
||||
let prepared: PreparedClusterCopilotClientConfiguration | undefined;
|
||||
try {
|
||||
prepared = prepareConfiguration(record.configFile as string);
|
||||
credentialBytes = readCredentialBytes(record.credentialFile as string);
|
||||
const response = await requestJson(
|
||||
prepared,
|
||||
Object.freeze({
|
||||
method: 'GET',
|
||||
path: record.path,
|
||||
requestId: record.requestId,
|
||||
authorization: `Bearer ${credentialBytes.toString('ascii')}`,
|
||||
}),
|
||||
MAXIMUM_RESPONSE_BYTES,
|
||||
normalizedOptions,
|
||||
);
|
||||
const requestId = responseRequestId(response, record.requestId);
|
||||
if (response.statusCode === 200) {
|
||||
if (
|
||||
!response.body ||
|
||||
typeof response.body !== 'object' ||
|
||||
Array.isArray(response.body)
|
||||
) {
|
||||
throw new ClusterCopilotClientRequestError();
|
||||
}
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
requestId,
|
||||
result: Object.freeze({
|
||||
...(response.body as Record<string, unknown>),
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (response.statusCode >= 400 && response.statusCode <= 599) {
|
||||
throw new ClusterCopilotClientRemoteError(
|
||||
response.statusCode,
|
||||
remoteCode(response.body),
|
||||
requestId,
|
||||
retryAfterSeconds(response.headers['retry-after']),
|
||||
);
|
||||
}
|
||||
throw new ClusterCopilotClientRequestError();
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof ClusterPluginPackageManagementClientConfigurationError
|
||||
) {
|
||||
throw new ClusterCopilotClientConfigurationError();
|
||||
}
|
||||
if (
|
||||
error instanceof ClusterCopilotClientConfigurationError ||
|
||||
error instanceof ClusterCopilotClientRequestError ||
|
||||
error instanceof ClusterCopilotClientRemoteError
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
throw new ClusterCopilotClientRequestError({
|
||||
cause: error instanceof Error ? error : undefined,
|
||||
});
|
||||
} finally {
|
||||
credentialBytes?.fill(0);
|
||||
prepared?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import {
|
||||
lstatSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
type PathLike,
|
||||
} from 'node:fs';
|
||||
import { lstatSync, readFileSync, realpathSync, type PathLike } from 'node:fs';
|
||||
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
||||
import { TextDecoder } from 'node:util';
|
||||
|
||||
@@ -28,19 +23,19 @@ const ASSETS = Object.freeze([
|
||||
name: 'index.html',
|
||||
field: 'html',
|
||||
maximumBytes: 32 * 1024,
|
||||
digest: 'f9fa959f30b92c6b000eecb744ce1d0a7fce822c62b3e17dcf10d4d579a072ac',
|
||||
digest: 'ed8db5c26dec23e7a5237ef1cd4f5f9c3fc9f5a04a4751b7a3e0ed22dac54c42',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'app.css',
|
||||
field: 'css',
|
||||
maximumBytes: 64 * 1024,
|
||||
digest: '200c3405e1e12329fcfb50509b31b19f1567a91552865f039ce0c2de1530032c',
|
||||
digest: '54234cbba7e110de2f68fad2abd657c334b7e3e80c5d9b4f59bda7e122b4b62f',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'app.js',
|
||||
field: 'javascript',
|
||||
maximumBytes: 32 * 1024,
|
||||
digest: 'd60913e725e767d9fa2cb65d60c0eae6d75d219f4bec8aad166bed8b6507fe02',
|
||||
digest: '61811eac6a89b097b67823ccf49b0736af6494be7b187dbdcbecfc59adb3fce0',
|
||||
}),
|
||||
] as const);
|
||||
|
||||
|
||||
@@ -2,13 +2,19 @@
|
||||
|
||||
import {
|
||||
executeClusterCopilotCommand,
|
||||
executeClusterProjectApiRead,
|
||||
probeClusterCopilotClientReadiness,
|
||||
validateClusterCopilotClientConfiguration,
|
||||
validateClusterCopilotClientCredentialFile,
|
||||
type ClusterCopilotClientCommand,
|
||||
} from '../copilot-client/client';
|
||||
import { readCanonicalFile } from '../management-support/managementClientConfiguration';
|
||||
import { loadClusterCopilotConsoleAssets } from './assets';
|
||||
import {
|
||||
CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS,
|
||||
clusterCopilotConsoleClientCommand,
|
||||
clusterCopilotConsoleProjectReadPath,
|
||||
type ClusterCopilotConsoleReadRequest,
|
||||
} from './contracts';
|
||||
import {
|
||||
clusterCopilotConsoleSessionDigest,
|
||||
startClusterCopilotConsoleServer,
|
||||
@@ -28,9 +34,7 @@ interface ClusterCopilotConsoleCliArguments {
|
||||
readonly check: boolean;
|
||||
readonly configFile: string;
|
||||
readonly credentialFile: string;
|
||||
readonly networkBoundary:
|
||||
| 'host-loopback'
|
||||
| 'container-published-loopback';
|
||||
readonly networkBoundary: 'host-loopback' | 'container-published-loopback';
|
||||
readonly sessionFile: string;
|
||||
readonly port: number;
|
||||
}
|
||||
@@ -154,11 +158,7 @@ export function parseClusterCopilotConsoleCliArguments(
|
||||
function readSessionDigest(sessionFile: string): Buffer {
|
||||
let bytes: Buffer | undefined;
|
||||
try {
|
||||
bytes = readCanonicalFile(
|
||||
sessionFile,
|
||||
MAXIMUM_SESSION_BYTES,
|
||||
'private',
|
||||
);
|
||||
bytes = readCanonicalFile(sessionFile, MAXIMUM_SESSION_BYTES, 'private');
|
||||
if (
|
||||
bytes.some((byte) => byte > 0x7f) ||
|
||||
!SESSION_TOKEN.test(bytes.toString('ascii'))
|
||||
@@ -199,7 +199,7 @@ async function main(): Promise<void> {
|
||||
publishedHostAddress: '127.0.0.1',
|
||||
browserCredential: 'forbidden',
|
||||
clusterCredential: 'server_only',
|
||||
operations: ['inspect', 'output'],
|
||||
operations: CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS,
|
||||
mutation: false,
|
||||
}) + '\n',
|
||||
);
|
||||
@@ -213,11 +213,19 @@ async function main(): Promise<void> {
|
||||
const server = await startClusterCopilotConsoleServer({
|
||||
assets,
|
||||
executor: Object.freeze({
|
||||
execute(command: Readonly<ClusterCopilotClientCommand>) {
|
||||
return executeClusterCopilotCommand({
|
||||
execute(request: Readonly<ClusterCopilotConsoleReadRequest>) {
|
||||
if (request.operation === 'inspect' || request.operation === 'output') {
|
||||
return executeClusterCopilotCommand({
|
||||
configFile: parsed.configFile,
|
||||
credentialFile: parsed.credentialFile,
|
||||
command: clusterCopilotConsoleClientCommand(request),
|
||||
});
|
||||
}
|
||||
return executeClusterProjectApiRead({
|
||||
configFile: parsed.configFile,
|
||||
credentialFile: parsed.credentialFile,
|
||||
command,
|
||||
path: clusterCopilotConsoleProjectReadPath(request),
|
||||
requestId: request.requestId,
|
||||
});
|
||||
},
|
||||
}),
|
||||
@@ -236,7 +244,7 @@ async function main(): Promise<void> {
|
||||
publishedHostAddress: '127.0.0.1',
|
||||
browserCredential: 'forbidden',
|
||||
clusterCredential: 'server_only',
|
||||
operations: ['inspect', 'output'],
|
||||
operations: CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS,
|
||||
mutation: false,
|
||||
}) + '\n',
|
||||
);
|
||||
|
||||
@@ -8,16 +8,85 @@ export const CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA =
|
||||
export const CLUSTER_COPILOT_CONSOLE_READ_RESPONSE_SCHEMA =
|
||||
'qinglong/cluster-copilot-console-read-response@v1' as const;
|
||||
|
||||
export type ClusterCopilotConsoleReadOperation = 'inspect' | 'output';
|
||||
export const CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS = Object.freeze([
|
||||
'inspect',
|
||||
'output',
|
||||
'run_list',
|
||||
'run_read',
|
||||
'run_event_list',
|
||||
'run_step_list',
|
||||
'task_list',
|
||||
'task_read',
|
||||
'workflow_list',
|
||||
'workflow_run_list',
|
||||
'workflow_run_read',
|
||||
'workflow_event_list',
|
||||
'workflow_step_list',
|
||||
] as const);
|
||||
|
||||
export interface ClusterCopilotConsoleReadRequest {
|
||||
export type ClusterCopilotConsoleReadOperation =
|
||||
(typeof CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS)[number];
|
||||
|
||||
interface BaseReadRequest<
|
||||
Operation extends ClusterCopilotConsoleReadOperation,
|
||||
> {
|
||||
readonly schema: typeof CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA;
|
||||
readonly operation: ClusterCopilotConsoleReadOperation;
|
||||
readonly operation: Operation;
|
||||
readonly projectId: string;
|
||||
readonly sourceRunId: string;
|
||||
readonly requestId: string;
|
||||
}
|
||||
|
||||
export type ClusterCopilotConsoleReadRequest =
|
||||
| (BaseReadRequest<'inspect'> & Readonly<{ sourceRunId: string }>)
|
||||
| (BaseReadRequest<'output'> & Readonly<{ sourceRunId: string }>)
|
||||
| (BaseReadRequest<'run_list'> &
|
||||
Readonly<{
|
||||
afterCreatedAtMs: number | null;
|
||||
afterRunId: string | null;
|
||||
limit: number;
|
||||
}>)
|
||||
| (BaseReadRequest<'run_read'> & Readonly<{ runId: string }>)
|
||||
| (BaseReadRequest<'run_event_list'> &
|
||||
Readonly<{ runId: string; afterSequence: number; limit: number }>)
|
||||
| (BaseReadRequest<'run_step_list'> &
|
||||
Readonly<{
|
||||
runId: string;
|
||||
afterStepKey: string | null;
|
||||
afterStepRunId: string | null;
|
||||
limit: number;
|
||||
}>)
|
||||
| (BaseReadRequest<'task_list'> &
|
||||
Readonly<{ afterTaskId: string | null; limit: number }>)
|
||||
| (BaseReadRequest<'task_read'> & Readonly<{ taskId: string }>)
|
||||
| (BaseReadRequest<'workflow_list'> & Readonly<{ packageName: string }>)
|
||||
| (BaseReadRequest<'workflow_run_list'> &
|
||||
Readonly<{
|
||||
packageName: string;
|
||||
workflowId: string;
|
||||
afterAdmittedAtMs: number | null;
|
||||
afterRunId: string | null;
|
||||
limit: number;
|
||||
}>)
|
||||
| (BaseReadRequest<'workflow_run_read'> &
|
||||
Readonly<{ packageName: string; workflowId: string; runId: string }>)
|
||||
| (BaseReadRequest<'workflow_event_list'> &
|
||||
Readonly<{
|
||||
packageName: string;
|
||||
workflowId: string;
|
||||
runId: string;
|
||||
afterSequence: number;
|
||||
limit: number;
|
||||
}>)
|
||||
| (BaseReadRequest<'workflow_step_list'> &
|
||||
Readonly<{
|
||||
packageName: string;
|
||||
workflowId: string;
|
||||
runId: string;
|
||||
afterStepKey: string | null;
|
||||
afterStepRunId: string | null;
|
||||
limit: number;
|
||||
}>);
|
||||
|
||||
export class InvalidClusterCopilotConsoleReadRequestError extends TypeError {
|
||||
readonly code = 'QL3_CLUSTER_COPILOT_CONSOLE_READ_REQUEST_INVALID';
|
||||
|
||||
@@ -28,47 +97,305 @@ export class InvalidClusterCopilotConsoleReadRequestError extends TypeError {
|
||||
}
|
||||
|
||||
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 COPILOT_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 WORKFLOW_ID = /^[a-z][a-z0-9-]{0,62}$/;
|
||||
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 InvalidClusterCopilotConsoleReadRequestError();
|
||||
}
|
||||
|
||||
export function normalizeClusterCopilotConsoleReadRequest(
|
||||
value: unknown,
|
||||
): Readonly<ClusterCopilotConsoleReadRequest> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return invalid();
|
||||
}
|
||||
const record = value as Record<string, unknown>;
|
||||
const keys = Object.keys(record).sort();
|
||||
function exact(
|
||||
record: Record<string, unknown>,
|
||||
operation: ClusterCopilotConsoleReadOperation,
|
||||
fields: readonly string[],
|
||||
): void {
|
||||
const actual = Object.keys(record).sort();
|
||||
const expected = [
|
||||
'operation',
|
||||
'projectId',
|
||||
'requestId',
|
||||
'schema',
|
||||
'sourceRunId',
|
||||
];
|
||||
...fields,
|
||||
].sort();
|
||||
if (
|
||||
keys.length !== expected.length ||
|
||||
keys.some((key, index) => key !== expected[index]) ||
|
||||
actual.length !== expected.length ||
|
||||
actual.some((key, index) => key !== expected[index]) ||
|
||||
record.schema !== CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA ||
|
||||
(record.operation !== 'inspect' && record.operation !== 'output') ||
|
||||
record.operation !== operation ||
|
||||
typeof record.projectId !== 'string' ||
|
||||
!IDENTITY.test(record.projectId) ||
|
||||
typeof record.sourceRunId !== 'string' ||
|
||||
!RUN_ID.test(record.sourceRunId) ||
|
||||
typeof record.requestId !== 'string' ||
|
||||
!IDENTITY.test(record.requestId)
|
||||
) {
|
||||
return invalid();
|
||||
}
|
||||
return Object.freeze({
|
||||
)
|
||||
invalid();
|
||||
}
|
||||
|
||||
function identifier(value: unknown): value is string {
|
||||
return typeof value === 'string' && IDENTITY.test(value);
|
||||
}
|
||||
|
||||
function limit(value: unknown): value is number {
|
||||
return (
|
||||
Number.isSafeInteger(value) && Number(value) >= 1 && Number(value) <= 64
|
||||
);
|
||||
}
|
||||
|
||||
function sequence(value: unknown): value is number {
|
||||
return (
|
||||
Number.isSafeInteger(value) &&
|
||||
Number(value) >= 0 &&
|
||||
Number(value) <= 2_147_483_647
|
||||
);
|
||||
}
|
||||
|
||||
function timestamp(value: unknown): value is number {
|
||||
return Number.isSafeInteger(value) && Number(value) >= 0;
|
||||
}
|
||||
|
||||
function common(record: Record<string, unknown>) {
|
||||
return {
|
||||
schema: CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA,
|
||||
operation: record.operation,
|
||||
projectId: record.projectId,
|
||||
sourceRunId: record.sourceRunId,
|
||||
requestId: record.requestId,
|
||||
projectId: record.projectId as string,
|
||||
requestId: record.requestId as string,
|
||||
} as const;
|
||||
}
|
||||
|
||||
function workflowTarget(
|
||||
record: Record<string, unknown>,
|
||||
requireRun: boolean,
|
||||
): boolean {
|
||||
return (
|
||||
typeof record.packageName === 'string' &&
|
||||
PACKAGE_NAME.test(record.packageName) &&
|
||||
typeof record.workflowId === 'string' &&
|
||||
WORKFLOW_ID.test(record.workflowId) &&
|
||||
(!requireRun ||
|
||||
(typeof record.runId === 'string' && UUID_V4.test(record.runId)))
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeClusterCopilotConsoleReadRequest(
|
||||
value: unknown,
|
||||
): Readonly<ClusterCopilotConsoleReadRequest> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) invalid();
|
||||
const record = value as Record<string, unknown>;
|
||||
const operation = record.operation;
|
||||
if (
|
||||
typeof operation !== 'string' ||
|
||||
!CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS.includes(
|
||||
operation as ClusterCopilotConsoleReadOperation,
|
||||
)
|
||||
)
|
||||
invalid();
|
||||
const op = operation as ClusterCopilotConsoleReadOperation;
|
||||
|
||||
if (op === 'inspect' || op === 'output') {
|
||||
exact(record, op, ['sourceRunId']);
|
||||
if (
|
||||
typeof record.sourceRunId !== 'string' ||
|
||||
!COPILOT_RUN_ID.test(record.sourceRunId)
|
||||
)
|
||||
invalid();
|
||||
return Object.freeze({
|
||||
...common(record),
|
||||
operation: op,
|
||||
sourceRunId: record.sourceRunId,
|
||||
});
|
||||
}
|
||||
if (op === 'run_list') {
|
||||
exact(record, op, ['afterCreatedAtMs', 'afterRunId', 'limit']);
|
||||
if (
|
||||
(record.afterCreatedAtMs === null) !== (record.afterRunId === null) ||
|
||||
(record.afterCreatedAtMs !== null &&
|
||||
!timestamp(record.afterCreatedAtMs)) ||
|
||||
(record.afterRunId !== null && !identifier(record.afterRunId)) ||
|
||||
!limit(record.limit)
|
||||
)
|
||||
invalid();
|
||||
return Object.freeze({
|
||||
...common(record),
|
||||
operation: op,
|
||||
afterCreatedAtMs: record.afterCreatedAtMs as number | null,
|
||||
afterRunId: record.afterRunId as string | null,
|
||||
limit: record.limit as number,
|
||||
});
|
||||
}
|
||||
if (op === 'run_read') {
|
||||
exact(record, op, ['runId']);
|
||||
if (!identifier(record.runId)) invalid();
|
||||
return Object.freeze({
|
||||
...common(record),
|
||||
operation: op,
|
||||
runId: record.runId,
|
||||
});
|
||||
}
|
||||
if (op === 'run_event_list') {
|
||||
exact(record, op, ['afterSequence', 'limit', 'runId']);
|
||||
if (
|
||||
!identifier(record.runId) ||
|
||||
!sequence(record.afterSequence) ||
|
||||
!limit(record.limit)
|
||||
)
|
||||
invalid();
|
||||
return Object.freeze({
|
||||
...common(record),
|
||||
operation: op,
|
||||
runId: record.runId,
|
||||
afterSequence: record.afterSequence as number,
|
||||
limit: record.limit as number,
|
||||
});
|
||||
}
|
||||
if (op === 'run_step_list') {
|
||||
exact(record, op, ['afterStepKey', 'afterStepRunId', 'limit', 'runId']);
|
||||
if (
|
||||
!identifier(record.runId) ||
|
||||
(record.afterStepKey === null) !== (record.afterStepRunId === null) ||
|
||||
(record.afterStepKey !== null && !identifier(record.afterStepKey)) ||
|
||||
(record.afterStepRunId !== null && !identifier(record.afterStepRunId)) ||
|
||||
!limit(record.limit)
|
||||
)
|
||||
invalid();
|
||||
return Object.freeze({
|
||||
...common(record),
|
||||
operation: op,
|
||||
runId: record.runId,
|
||||
afterStepKey: record.afterStepKey as string | null,
|
||||
afterStepRunId: record.afterStepRunId as string | null,
|
||||
limit: record.limit as number,
|
||||
});
|
||||
}
|
||||
if (op === 'task_list') {
|
||||
exact(record, op, ['afterTaskId', 'limit']);
|
||||
if (
|
||||
(record.afterTaskId !== null && !identifier(record.afterTaskId)) ||
|
||||
!limit(record.limit)
|
||||
)
|
||||
invalid();
|
||||
return Object.freeze({
|
||||
...common(record),
|
||||
operation: op,
|
||||
afterTaskId: record.afterTaskId as string | null,
|
||||
limit: record.limit as number,
|
||||
});
|
||||
}
|
||||
if (op === 'task_read') {
|
||||
exact(record, op, ['taskId']);
|
||||
if (!identifier(record.taskId)) invalid();
|
||||
return Object.freeze({
|
||||
...common(record),
|
||||
operation: op,
|
||||
taskId: record.taskId,
|
||||
});
|
||||
}
|
||||
if (op === 'workflow_list') {
|
||||
exact(record, op, ['packageName']);
|
||||
if (
|
||||
typeof record.packageName !== 'string' ||
|
||||
!PACKAGE_NAME.test(record.packageName)
|
||||
)
|
||||
invalid();
|
||||
return Object.freeze({
|
||||
...common(record),
|
||||
operation: op,
|
||||
packageName: record.packageName,
|
||||
});
|
||||
}
|
||||
if (op === 'workflow_run_list') {
|
||||
exact(record, op, [
|
||||
'afterAdmittedAtMs',
|
||||
'afterRunId',
|
||||
'limit',
|
||||
'packageName',
|
||||
'workflowId',
|
||||
]);
|
||||
if (
|
||||
!workflowTarget(record, false) ||
|
||||
(record.afterAdmittedAtMs === null) !== (record.afterRunId === null) ||
|
||||
(record.afterAdmittedAtMs !== null &&
|
||||
!timestamp(record.afterAdmittedAtMs)) ||
|
||||
(record.afterRunId !== null &&
|
||||
(typeof record.afterRunId !== 'string' ||
|
||||
!UUID_V4.test(record.afterRunId))) ||
|
||||
!limit(record.limit)
|
||||
)
|
||||
invalid();
|
||||
return Object.freeze({
|
||||
...common(record),
|
||||
operation: op,
|
||||
packageName: record.packageName as string,
|
||||
workflowId: record.workflowId as string,
|
||||
afterAdmittedAtMs: record.afterAdmittedAtMs as number | null,
|
||||
afterRunId: record.afterRunId as string | null,
|
||||
limit: record.limit as number,
|
||||
});
|
||||
}
|
||||
if (op === 'workflow_run_read') {
|
||||
exact(record, op, ['packageName', 'runId', 'workflowId']);
|
||||
if (!workflowTarget(record, true)) invalid();
|
||||
return Object.freeze({
|
||||
...common(record),
|
||||
operation: op,
|
||||
packageName: record.packageName as string,
|
||||
workflowId: record.workflowId as string,
|
||||
runId: record.runId as string,
|
||||
});
|
||||
}
|
||||
if (op === 'workflow_event_list') {
|
||||
exact(record, op, [
|
||||
'afterSequence',
|
||||
'limit',
|
||||
'packageName',
|
||||
'runId',
|
||||
'workflowId',
|
||||
]);
|
||||
if (
|
||||
!workflowTarget(record, true) ||
|
||||
!sequence(record.afterSequence) ||
|
||||
!limit(record.limit)
|
||||
)
|
||||
invalid();
|
||||
return Object.freeze({
|
||||
...common(record),
|
||||
operation: op,
|
||||
packageName: record.packageName as string,
|
||||
workflowId: record.workflowId as string,
|
||||
runId: record.runId as string,
|
||||
afterSequence: record.afterSequence as number,
|
||||
limit: record.limit as number,
|
||||
});
|
||||
}
|
||||
exact(record, 'workflow_step_list', [
|
||||
'afterStepKey',
|
||||
'afterStepRunId',
|
||||
'limit',
|
||||
'packageName',
|
||||
'runId',
|
||||
'workflowId',
|
||||
]);
|
||||
if (
|
||||
!workflowTarget(record, true) ||
|
||||
(record.afterStepKey === null) !== (record.afterStepRunId === null) ||
|
||||
(record.afterStepKey !== null &&
|
||||
(typeof record.afterStepKey !== 'string' ||
|
||||
!WORKFLOW_ID.test(record.afterStepKey))) ||
|
||||
(record.afterStepRunId !== null &&
|
||||
(typeof record.afterStepRunId !== 'string' ||
|
||||
!UUID_V4.test(record.afterStepRunId))) ||
|
||||
!limit(record.limit)
|
||||
)
|
||||
invalid();
|
||||
return Object.freeze({
|
||||
...common(record),
|
||||
operation: 'workflow_step_list',
|
||||
packageName: record.packageName as string,
|
||||
workflowId: record.workflowId as string,
|
||||
runId: record.runId as string,
|
||||
afterStepKey: record.afterStepKey as string | null,
|
||||
afterStepRunId: record.afterStepRunId as string | null,
|
||||
limit: record.limit as number,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -76,6 +403,8 @@ export function clusterCopilotConsoleClientCommand(
|
||||
request: Readonly<ClusterCopilotConsoleReadRequest>,
|
||||
): Readonly<ClusterCopilotClientCommand> {
|
||||
const normalized = normalizeClusterCopilotConsoleReadRequest(request);
|
||||
if (normalized.operation !== 'inspect' && normalized.operation !== 'output')
|
||||
invalid();
|
||||
return Object.freeze({
|
||||
schema: CLUSTER_COPILOT_CLIENT_COMMAND_SCHEMA,
|
||||
operation: normalized.operation,
|
||||
@@ -84,3 +413,111 @@ export function clusterCopilotConsoleClientCommand(
|
||||
requestId: normalized.requestId,
|
||||
});
|
||||
}
|
||||
|
||||
function encoded(value: string): string {
|
||||
return encodeURIComponent(value);
|
||||
}
|
||||
|
||||
function query(
|
||||
entries: readonly (readonly [string, string | number])[],
|
||||
): string {
|
||||
return entries.length === 0
|
||||
? ''
|
||||
: '?' +
|
||||
entries
|
||||
.map(([key, value]) => encoded(key) + '=' + encoded(String(value)))
|
||||
.join('&');
|
||||
}
|
||||
|
||||
export function clusterCopilotConsoleProjectReadPath(
|
||||
request: Readonly<ClusterCopilotConsoleReadRequest>,
|
||||
): string {
|
||||
const normalized = normalizeClusterCopilotConsoleReadRequest(request);
|
||||
if (normalized.operation === 'inspect' || normalized.operation === 'output')
|
||||
invalid();
|
||||
const project = '/api/v3/projects/' + encoded(normalized.projectId);
|
||||
if (normalized.operation === 'run_list') {
|
||||
const cursor: (readonly [string, string | number])[] =
|
||||
normalized.afterCreatedAtMs === null
|
||||
? []
|
||||
: [
|
||||
['after_created_at_ms', normalized.afterCreatedAtMs],
|
||||
['after_run_id', normalized.afterRunId!],
|
||||
];
|
||||
return project + '/runs' + query([...cursor, ['limit', normalized.limit]]);
|
||||
}
|
||||
if (normalized.operation === 'run_read')
|
||||
return project + '/runs/' + encoded(normalized.runId);
|
||||
if (normalized.operation === 'run_event_list')
|
||||
return (
|
||||
project +
|
||||
'/runs/' +
|
||||
encoded(normalized.runId) +
|
||||
'/events' +
|
||||
query([
|
||||
['after_sequence', normalized.afterSequence],
|
||||
['limit', normalized.limit],
|
||||
])
|
||||
);
|
||||
if (normalized.operation === 'run_step_list') {
|
||||
const cursor: (readonly [string, string | number])[] =
|
||||
normalized.afterStepKey === null
|
||||
? []
|
||||
: [
|
||||
['after_step_key', normalized.afterStepKey],
|
||||
['after_step_run_id', normalized.afterStepRunId!],
|
||||
];
|
||||
return (
|
||||
project +
|
||||
'/runs/' +
|
||||
encoded(normalized.runId) +
|
||||
'/steps' +
|
||||
query([...cursor, ['limit', normalized.limit]])
|
||||
);
|
||||
}
|
||||
if (normalized.operation === 'task_list') {
|
||||
const cursor: (readonly [string, string | number])[] =
|
||||
normalized.afterTaskId === null
|
||||
? []
|
||||
: [['after_task_id', normalized.afterTaskId]];
|
||||
return project + '/tasks' + query([...cursor, ['limit', normalized.limit]]);
|
||||
}
|
||||
if (normalized.operation === 'task_read')
|
||||
return project + '/tasks/' + encoded(normalized.taskId);
|
||||
const packageRoot = project + '/packages/' + encoded(normalized.packageName);
|
||||
if (normalized.operation === 'workflow_list')
|
||||
return packageRoot + '/workflows';
|
||||
const workflowRoot =
|
||||
packageRoot + '/workflows/' + encoded(normalized.workflowId);
|
||||
if (normalized.operation === 'workflow_run_list') {
|
||||
const cursor: (readonly [string, string | number])[] =
|
||||
normalized.afterAdmittedAtMs === null
|
||||
? []
|
||||
: [
|
||||
['after_admitted_at_ms', normalized.afterAdmittedAtMs],
|
||||
['after_run_id', normalized.afterRunId!],
|
||||
];
|
||||
return (
|
||||
workflowRoot + '/runs' + query([...cursor, ['limit', normalized.limit]])
|
||||
);
|
||||
}
|
||||
const runRoot = workflowRoot + '/runs/' + encoded(normalized.runId);
|
||||
if (normalized.operation === 'workflow_run_read') return runRoot;
|
||||
if (normalized.operation === 'workflow_event_list')
|
||||
return (
|
||||
runRoot +
|
||||
'/events' +
|
||||
query([
|
||||
['after_sequence', normalized.afterSequence],
|
||||
['limit', normalized.limit],
|
||||
])
|
||||
);
|
||||
const cursor: (readonly [string, string | number])[] =
|
||||
normalized.afterStepKey === null
|
||||
? []
|
||||
: [
|
||||
['after_step_key', normalized.afterStepKey],
|
||||
['after_step_run_id', normalized.afterStepRunId!],
|
||||
];
|
||||
return runRoot + '/steps' + query([...cursor, ['limit', normalized.limit]]);
|
||||
}
|
||||
|
||||
@@ -9,17 +9,14 @@ import {
|
||||
ClusterCopilotClientConfigurationError,
|
||||
ClusterCopilotClientRemoteError,
|
||||
ClusterCopilotClientRequestError,
|
||||
type ClusterCopilotClientCommand,
|
||||
type ClusterCopilotClientResult,
|
||||
} from '../copilot-client/client';
|
||||
import {
|
||||
type ClusterCopilotConsoleAssets,
|
||||
} from './assets';
|
||||
import { type ClusterCopilotConsoleAssets } from './assets';
|
||||
import {
|
||||
CLUSTER_COPILOT_CONSOLE_READ_RESPONSE_SCHEMA,
|
||||
InvalidClusterCopilotConsoleReadRequestError,
|
||||
clusterCopilotConsoleClientCommand,
|
||||
normalizeClusterCopilotConsoleReadRequest,
|
||||
type ClusterCopilotConsoleReadOperation,
|
||||
type ClusterCopilotConsoleReadRequest,
|
||||
} from './contracts';
|
||||
|
||||
export const CLUSTER_COPILOT_CONSOLE_LIMITS = Object.freeze({
|
||||
@@ -31,9 +28,13 @@ export const CLUSTER_COPILOT_CONSOLE_LIMITS = Object.freeze({
|
||||
});
|
||||
|
||||
export interface ClusterCopilotConsoleExecutor {
|
||||
execute(
|
||||
command: Readonly<ClusterCopilotClientCommand>,
|
||||
): Promise<Readonly<ClusterCopilotClientResult>>;
|
||||
execute(request: Readonly<ClusterCopilotConsoleReadRequest>): Promise<
|
||||
Readonly<{
|
||||
schemaVersion: 1;
|
||||
requestId: string;
|
||||
result: Readonly<Record<string, unknown>>;
|
||||
}>
|
||||
>;
|
||||
}
|
||||
|
||||
export interface ClusterCopilotConsoleServerOptions {
|
||||
@@ -111,10 +112,7 @@ export function clusterCopilotConsoleSessionDigest(value: string): Buffer {
|
||||
return invalid();
|
||||
}
|
||||
const decoded = Buffer.from(value, 'base64url');
|
||||
if (
|
||||
decoded.byteLength !== 32 ||
|
||||
decoded.toString('base64url') !== value
|
||||
) {
|
||||
if (decoded.byteLength !== 32 || decoded.toString('base64url') !== value) {
|
||||
decoded.fill(0);
|
||||
return invalid();
|
||||
}
|
||||
@@ -125,7 +123,9 @@ export function clusterCopilotConsoleSessionDigest(value: string): Buffer {
|
||||
.digest();
|
||||
}
|
||||
|
||||
function securityHeaders(contentType: string): Readonly<Record<string, string>> {
|
||||
function securityHeaders(
|
||||
contentType: string,
|
||||
): Readonly<Record<string, string>> {
|
||||
return Object.freeze({
|
||||
'cache-control': 'no-store',
|
||||
'content-security-policy': CONTENT_SECURITY_POLICY,
|
||||
@@ -181,11 +181,29 @@ function headerCount(request: IncomingMessage, name: string): number {
|
||||
return count;
|
||||
}
|
||||
|
||||
function targetPath(request: IncomingMessage): 'inspect' | 'output' | null {
|
||||
const READ_ROUTES: Readonly<
|
||||
Record<string, ClusterCopilotConsoleReadOperation>
|
||||
> = Object.freeze({
|
||||
'/api/v1/copilot/inspect': 'inspect',
|
||||
'/api/v1/copilot/output': 'output',
|
||||
'/api/v1/observe/run-list': 'run_list',
|
||||
'/api/v1/observe/run': 'run_read',
|
||||
'/api/v1/observe/run-events': 'run_event_list',
|
||||
'/api/v1/observe/run-steps': 'run_step_list',
|
||||
'/api/v1/observe/task-list': 'task_list',
|
||||
'/api/v1/observe/task': 'task_read',
|
||||
'/api/v1/observe/workflow-list': 'workflow_list',
|
||||
'/api/v1/observe/workflow-run-list': 'workflow_run_list',
|
||||
'/api/v1/observe/workflow-run': 'workflow_run_read',
|
||||
'/api/v1/observe/workflow-events': 'workflow_event_list',
|
||||
'/api/v1/observe/workflow-steps': 'workflow_step_list',
|
||||
});
|
||||
|
||||
function targetPath(
|
||||
request: IncomingMessage,
|
||||
): ClusterCopilotConsoleReadOperation | null {
|
||||
if (request.method !== 'POST') return null;
|
||||
if (request.url === '/api/v1/copilot/inspect') return 'inspect';
|
||||
if (request.url === '/api/v1/copilot/output') return 'output';
|
||||
return null;
|
||||
return request.url === undefined ? null : READ_ROUTES[request.url] ?? null;
|
||||
}
|
||||
|
||||
function authorize(
|
||||
@@ -279,11 +297,7 @@ function remoteFailure(
|
||||
error: ClusterCopilotClientRemoteError,
|
||||
): void {
|
||||
const statusCode =
|
||||
error.statusCode === 404
|
||||
? 404
|
||||
: error.statusCode === 429
|
||||
? 429
|
||||
: 502;
|
||||
error.statusCode === 404 ? 404 : error.statusCode === 429 ? 429 : 502;
|
||||
sendJson(
|
||||
response,
|
||||
statusCode,
|
||||
@@ -377,9 +391,7 @@ export async function startClusterCopilotConsoleServer(
|
||||
request.resume();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
inFlight >= CLUSTER_COPILOT_CONSOLE_LIMITS.maximumConcurrentRequests
|
||||
) {
|
||||
if (inFlight >= CLUSTER_COPILOT_CONSOLE_LIMITS.maximumConcurrentRequests) {
|
||||
sendJson(
|
||||
response,
|
||||
429,
|
||||
@@ -400,9 +412,7 @@ export async function startClusterCopilotConsoleServer(
|
||||
if (normalized.operation !== operation) {
|
||||
throw new InvalidClusterCopilotConsoleReadRequestError();
|
||||
}
|
||||
const result = await executor.execute(
|
||||
clusterCopilotConsoleClientCommand(normalized),
|
||||
);
|
||||
const result = await executor.execute(normalized);
|
||||
const envelope = Object.freeze({
|
||||
schema: CLUSTER_COPILOT_CONSOLE_READ_RESPONSE_SCHEMA,
|
||||
operation,
|
||||
@@ -416,12 +426,7 @@ export async function startClusterCopilotConsoleServer(
|
||||
) {
|
||||
throw new ClusterCopilotClientRequestError();
|
||||
}
|
||||
send(
|
||||
response,
|
||||
200,
|
||||
'application/json; charset=utf-8',
|
||||
encoded,
|
||||
);
|
||||
send(response, 200, 'application/json; charset=utf-8', encoded);
|
||||
} catch (error) {
|
||||
if (response.headersSent) {
|
||||
response.destroy();
|
||||
|
||||
Reference in New Issue
Block a user