feat(ql3): add explicit cluster observation console

This commit is contained in:
whyour
2026-08-16 06:09:54 +08:00
parent fba8dfb602
commit 7955d55629
20 changed files with 1881 additions and 877 deletions
@@ -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();