feat(ql3): add profile-aware run log reads

This commit is contained in:
whyour
2026-08-12 01:43:14 +08:00
parent c699c32461
commit 308aa75d89
33 changed files with 2880 additions and 306 deletions
@@ -22,6 +22,7 @@ import type { LocalApiRunListRoute } from '../run/runListRoute';
import type { LocalApiRunReadRoute } from '../run/runReadRoute';
import type { LocalApiRunStepListRoute } from '../run/runStepListRoute';
import type { LocalApiRunCancellationRoute } from '../run/runCancellationRoute';
import type { LocalApiRunAttemptLogReadRoute } from '../run/runAttemptLogReadRoute';
import type { LocalApiTaskListRoute } from '../task/taskListRoute';
import type { LocalApiTaskReadRoute } from '../task/taskReadRoute';
import type { LocalApiTaskStartRoute } from '../task/taskStartRoute';
@@ -50,6 +51,14 @@ export type LocalApiAdmissionOperation =
runId: string;
input: Readonly<BoundedRunStepListInput>;
}>
| Readonly<{
operationId: 'run.log.read';
projectId: string;
runId: string;
attemptId: string;
offset: number;
length: number;
}>
| Readonly<{
operationId: 'run.cancel';
projectId: string;
@@ -99,6 +108,7 @@ export interface LocalApiAdmissionOptions {
readonly runEventListRoute: LocalApiRunEventListRoute;
readonly runStepListRoute: LocalApiRunStepListRoute;
readonly runCancellationRoute: LocalApiRunCancellationRoute;
readonly runAttemptLogReadRoute: LocalApiRunAttemptLogReadRoute;
readonly taskListRoute: LocalApiTaskListRoute;
readonly taskReadRoute: LocalApiTaskReadRoute;
readonly taskStartRoute: LocalApiTaskStartRoute;
@@ -176,6 +186,7 @@ export function createLocalApiAdmission(
typeof options.runEventListRoute?.handle !== 'function' ||
typeof options.runStepListRoute?.handle !== 'function' ||
typeof options.runCancellationRoute?.handle !== 'function' ||
typeof options.runAttemptLogReadRoute?.handle !== 'function' ||
typeof options.taskListRoute?.handle !== 'function' ||
typeof options.taskReadRoute?.handle !== 'function' ||
typeof options.taskStartRoute?.handle !== 'function' ||
@@ -236,12 +247,14 @@ export function createLocalApiAdmission(
request.operation.projectId,
request.operation.operationId === 'run.cancel'
? 'run.stop'
: request.operation.operationId === 'run.log.read'
? 'artifact.read'
: request.operation.operationId === 'task.start'
? 'run.start'
? 'run.start'
: request.operation.operationId === 'task.list' ||
request.operation.operationId === 'task.get'
? 'task.read'
: 'run.read',
request.operation.operationId === 'task.get'
? 'task.read'
: 'run.read',
),
);
} catch {
@@ -279,9 +292,15 @@ export function createLocalApiAdmission(
),
);
if (auditFailure) return auditFailure;
if (decision.effect === 'deny') return response(403, 'forbidden');
if (decision.effect === 'deny') {
return request.operation.operationId === 'run.log.read'
? response(404, 'artifact_not_found')
: response(403, 'forbidden');
}
if (decision.effect === 'require_approval') {
return response(403, 'approval_required');
return request.operation.operationId === 'run.log.read'
? response(404, 'artifact_not_found')
: response(403, 'approval_required');
}
if (request.signal.aborted) return response(503, 'request_unavailable');
try {
@@ -337,6 +356,16 @@ export function createLocalApiAdmission(
principal: authenticated.principal,
policyFence: decision.fence,
});
case 'run.log.read':
if (body !== null) return response(400, 'invalid_request_body');
return options.runAttemptLogReadRoute.handle({
projectId: request.operation.projectId,
runId: request.operation.runId,
attemptId: request.operation.attemptId,
offset: request.operation.offset,
length: request.operation.length,
signal: request.signal,
});
case 'task.list':
if (body !== null) return response(400, 'invalid_request_body');
return options.taskListRoute.handle({
@@ -15,6 +15,7 @@ import { createLocalApiRunReadRoute } from '../run/runReadRoute';
import { createLocalApiRunEventListRoute } from '../run/runEventListRoute';
import { createLocalApiRunStepListRoute } from '../run/runStepListRoute';
import { createLocalApiRunCancellationRoute } from '../run/runCancellationRoute';
import { createLocalApiRunAttemptLogReadRoute } from '../run/runAttemptLogReadRoute';
import { createLocalApiTaskListRoute } from '../task/taskListRoute';
import { createLocalApiTaskReadRoute } from '../task/taskReadRoute';
import { createLocalApiTaskStartRoute } from '../task/taskStartRoute';
@@ -109,6 +110,9 @@ export function createLocalApiProductSurface(
authority.runCancellation,
options.randomUuid ?? randomUUID,
);
const runAttemptLogReadRoute = createLocalApiRunAttemptLogReadRoute(
authority.runAttemptLogRead,
);
const taskListRoute = createLocalApiTaskListRoute(
authority.taskDefinitions,
);
@@ -128,6 +132,7 @@ export function createLocalApiProductSurface(
runEventListRoute,
runStepListRoute,
runCancellationRoute,
runAttemptLogReadRoute,
taskListRoute,
taskReadRoute,
taskStartRoute,
@@ -0,0 +1,111 @@
import {
InvalidRunAttemptLogReadError,
RunAttemptLogReadUnavailableError,
type RunAttemptLogReadRequest,
type RunAttemptLogReadResult,
} from '@qinglong/runtime-core/run-attempt-log-read';
import type { LocalApiResponse } from '../transport/contract';
export interface LocalApiRunAttemptLogReadCapability {
read(
request: Readonly<RunAttemptLogReadRequest>,
): Promise<RunAttemptLogReadResult>;
}
export interface LocalApiRunAttemptLogReadRequest {
readonly projectId: string;
readonly runId: string;
readonly attemptId: string;
readonly offset: number;
readonly length: number;
readonly signal?: AbortSignal;
}
export interface LocalApiRunAttemptLogReadRoute {
handle(
request: Readonly<LocalApiRunAttemptLogReadRequest>,
): Promise<LocalApiResponse>;
}
function response(
statusCode: number,
body: Readonly<Record<string, unknown>>,
): LocalApiResponse {
return Object.freeze({ statusCode, body: Object.freeze(body) });
}
function projection(
result: Extract<RunAttemptLogReadResult, { readonly status: 'available' }>,
): Readonly<Record<string, unknown>> {
return Object.freeze({
schema: 'qinglong/run-attempt-log-read-result@v1',
status: 'available',
projectId: result.projectId,
runId: result.runId,
attemptId: result.attemptId,
range: Object.freeze({
start: result.start,
endExclusive: result.endExclusive,
totalBytes: result.totalBytes,
...(result.nextOffset === undefined
? {}
: { nextOffset: result.nextOffset }),
}),
encoding: 'base64',
content: Buffer.from(
result.content.buffer,
result.content.byteOffset,
result.content.byteLength,
).toString('base64'),
truncation: result.truncation,
});
}
export function createLocalApiRunAttemptLogReadRoute(
capability: LocalApiRunAttemptLogReadCapability,
): Readonly<LocalApiRunAttemptLogReadRoute> {
if (!capability || typeof capability.read !== 'function') {
throw new TypeError('Local API Run Attempt log read capability is invalid');
}
return Object.freeze({
async handle(request: Readonly<LocalApiRunAttemptLogReadRequest>) {
try {
const result = await capability.read({
projectId: request.projectId,
runId: request.runId,
attemptId: request.attemptId,
range: Object.freeze({
offset: request.offset,
length: request.length,
}),
...(request.signal === undefined ? {} : { signal: request.signal }),
});
if (result.status === 'not_found') {
return response(404, { code: 'artifact_not_found' });
}
if (result.status === 'pending') {
return response(202, {
schema: 'qinglong/run-attempt-log-read-result@v1',
status: 'pending',
projectId: result.projectId,
runId: result.runId,
attemptId: result.attemptId,
});
}
if (result.status === 'missing') {
return response(503, { code: 'artifact_unavailable' });
}
return response(200, projection(result));
} catch (error) {
if (error instanceof InvalidRunAttemptLogReadError) {
return response(400, { code: 'invalid_run_log_read_request' });
}
if (error instanceof RunAttemptLogReadUnavailableError) {
return response(503, { code: 'artifact_unavailable' });
}
return response(503, { code: 'artifact_unavailable' });
}
},
});
}
@@ -28,6 +28,8 @@ const RUN_STEP_LIST_ROUTE_PATTERN =
/^\/api\/v3\/projects\/([A-Za-z0-9][A-Za-z0-9._:-]{0,127})\/runs\/([A-Za-z0-9][A-Za-z0-9._:-]{0,127})\/steps$/;
const RUN_CANCELLATION_ROUTE_PATTERN =
/^\/api\/v3\/projects\/([A-Za-z0-9][A-Za-z0-9._:-]{0,127})\/runs\/([A-Za-z0-9][A-Za-z0-9._:-]{0,127})\/cancellation$/;
const RUN_ATTEMPT_LOG_READ_ROUTE_PATTERN =
/^\/api\/v3\/projects\/([A-Za-z0-9][A-Za-z0-9._:-]{0,127})\/runs\/([A-Za-z0-9][A-Za-z0-9._:-]{0,127})\/attempts\/([A-Za-z0-9][A-Za-z0-9._:-]{0,127})\/log$/;
const TASK_LIST_ROUTE_PATTERN =
/^\/api\/v3\/projects\/([A-Za-z0-9][A-Za-z0-9._:-]{0,127})\/tasks$/;
const TASK_READ_ROUTE_PATTERN =
@@ -44,6 +46,7 @@ type LocalApiRouteResolution =
| 'invalid_run_list_query'
| 'invalid_run_event_list_query'
| 'invalid_run_step_list_query'
| 'invalid_run_log_read_query'
| 'invalid_task_list_query';
}>;
@@ -340,10 +343,7 @@ function parseTaskListQuery(
}
const name = field.slice(0, separator);
const value = field.slice(separator + 1);
if (
values.has(name) ||
(name !== 'limit' && name !== 'after_task_id')
) {
if (values.has(name) || (name !== 'limit' && name !== 'after_task_id')) {
throw new TypeError();
}
values.set(name, value);
@@ -363,13 +363,58 @@ function parseTaskListQuery(
}
return Object.freeze({
...(limit === undefined ? {} : { limit }),
...(taskId === undefined
? {}
: { after: Object.freeze({ taskId }) }),
...(taskId === undefined ? {} : { after: Object.freeze({ taskId }) }),
});
}
function route(request: IncomingMessage): LocalApiRouteResolution | null {
function parseRunAttemptLogReadQuery(
rawQuery: string | undefined,
profile: LocalApplicationProfile,
): Readonly<{ offset: number; length: number }> {
const defaultLength = profile === 'edge' ? 16 * 1024 : 32 * 1024;
if (rawQuery === undefined) {
return Object.freeze({ offset: 0, length: defaultLength });
}
if (rawQuery.length === 0) throw new TypeError();
const values = new Map<string, string>();
for (const field of rawQuery.split('&')) {
const separator = field.indexOf('=');
if (
separator < 1 ||
separator !== field.lastIndexOf('=') ||
separator === field.length - 1
) {
throw new TypeError();
}
const name = field.slice(0, separator);
const value = field.slice(separator + 1);
if (values.has(name) || (name !== 'offset' && name !== 'length')) {
throw new TypeError();
}
values.set(name, value);
}
const rawOffset = values.get('offset');
const offset = rawOffset === undefined ? 0 : Number(rawOffset);
const rawLength = values.get('length');
const length = rawLength === undefined ? defaultLength : Number(rawLength);
if (
!Number.isSafeInteger(offset) ||
offset < 0 ||
(rawOffset !== undefined && String(offset) !== rawOffset) ||
!Number.isSafeInteger(length) ||
length < 1 ||
length > 32 * 1024 ||
(rawLength !== undefined && String(length) !== rawLength)
) {
throw new TypeError();
}
return Object.freeze({ offset, length });
}
function route(
request: IncomingMessage,
profile: LocalApplicationProfile,
): LocalApiRouteResolution | null {
const rawUrl = request.url;
if (
typeof rawUrl !== 'string' ||
@@ -403,6 +448,20 @@ function route(request: IncomingMessage): LocalApiRouteResolution | null {
: null;
}
if (request.method !== 'GET') return null;
const runAttemptLogReadMatch = RUN_ATTEMPT_LOG_READ_ROUTE_PATTERN.exec(path);
if (runAttemptLogReadMatch) {
try {
return Object.freeze({
operationId: 'run.log.read',
projectId: runAttemptLogReadMatch[1]!,
runId: runAttemptLogReadMatch[2]!,
attemptId: runAttemptLogReadMatch[3]!,
...parseRunAttemptLogReadQuery(rawQuery, profile),
});
} catch {
return Object.freeze({ errorCode: 'invalid_run_log_read_query' });
}
}
const taskReadMatch = TASK_READ_ROUTE_PATTERN.exec(path);
if (taskReadMatch) {
return rawQuery === undefined
@@ -552,7 +611,7 @@ export async function startLocalApiHttpSurface(
send(response, requestId, errorResponse(503, 'server_overloaded'));
return;
}
const resolvedRoute = route(request);
const resolvedRoute = route(request, options.profile);
if (!resolvedRoute) {
send(response, requestId, errorResponse(404, 'route_not_found'));
return;
@@ -608,9 +667,9 @@ export async function startLocalApiHttpSurface(
error instanceof RangeError
? 'request_body_too_large'
: error instanceof Error &&
error.message === 'request_unavailable'
? 'request_unavailable'
: 'invalid_request_body';
error.message === 'request_unavailable'
? 'request_unavailable'
: 'invalid_request_body';
send(
response,
requestId,
@@ -618,8 +677,8 @@ export async function startLocalApiHttpSurface(
code === 'request_body_too_large'
? 413
: code === 'request_unavailable'
? 503
: 400,
? 503
: 400,
code,
),
);