mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-23 03:18:09 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
import {
|
||||
APPROVAL_DECISION_MODES,
|
||||
APPROVAL_REQUEST_STATES,
|
||||
APPROVAL_RISKS,
|
||||
normalizeApprovalRequestRecord,
|
||||
} from '@qinglong/runtime-core/approved-action';
|
||||
import type {
|
||||
ApprovalRequestDetailSource,
|
||||
} from '@qinglong/runtime-core/approval-discovery';
|
||||
import {
|
||||
normalizeApprovalDetailPreview,
|
||||
} from '@qinglong/runtime-core/approval-discovery';
|
||||
import { normalizeProjectPermission } from '@qinglong/runtime-core/project-policy';
|
||||
import { SECURITY_SUBJECT_TYPES } from '@qinglong/runtime-core/security';
|
||||
import {
|
||||
normalizeToolDefinition,
|
||||
type ToolJsonValue,
|
||||
} from '@qinglong/runtime-core/tool-registry';
|
||||
|
||||
export const BUILTIN_APPROVAL_GET_TOOL = Object.freeze({
|
||||
name: 'qinglong.approval.get',
|
||||
version: '1.0.0',
|
||||
});
|
||||
export const BUILTIN_APPROVAL_GET_TIMEOUT_SECONDS = 5;
|
||||
|
||||
const MAX_INT = 2_147_483_647;
|
||||
const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
||||
const PREVIEW_FIELD_KINDS = ['count', 'identifier', 'redacted', 'text'] as const;
|
||||
|
||||
const PREVIEW_SCHEMA = Object.freeze({
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string', minLength: 1, maxLength: 256 },
|
||||
summary: { type: 'string', minLength: 1, maxLength: 2048 },
|
||||
fields: {
|
||||
type: 'array',
|
||||
maxItems: 16,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
kind: {
|
||||
type: 'string',
|
||||
maxLength: 16,
|
||||
enum: [...PREVIEW_FIELD_KINDS],
|
||||
},
|
||||
label: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
value: { type: 'string', minLength: 1, maxLength: 512 },
|
||||
},
|
||||
required: ['kind', 'label'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
warnings: {
|
||||
type: 'array',
|
||||
maxItems: 8,
|
||||
items: { type: 'string', minLength: 1, maxLength: 64 },
|
||||
},
|
||||
},
|
||||
required: ['title', 'summary', 'fields', 'warnings'],
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
export const BUILTIN_APPROVAL_GET_TOOL_DEFINITION = normalizeToolDefinition({
|
||||
name: BUILTIN_APPROVAL_GET_TOOL.name,
|
||||
version: BUILTIN_APPROVAL_GET_TOOL.version,
|
||||
description:
|
||||
'Get one Approval and its bounded redacted Tool preview in the authenticated Project',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
requestId: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
},
|
||||
required: ['requestId'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
outputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
found: { type: 'boolean' },
|
||||
approval: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
requestId: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
version: { type: 'integer', minimum: 1, maximum: MAX_INT },
|
||||
state: {
|
||||
type: 'string',
|
||||
maxLength: 32,
|
||||
enum: [...APPROVAL_REQUEST_STATES],
|
||||
},
|
||||
risk: {
|
||||
type: 'string',
|
||||
maxLength: 32,
|
||||
enum: [...APPROVAL_RISKS],
|
||||
},
|
||||
decisionMode: {
|
||||
type: 'string',
|
||||
maxLength: 32,
|
||||
enum: [...APPROVAL_DECISION_MODES],
|
||||
},
|
||||
permission: { type: 'string', minLength: 1, maxLength: 255 },
|
||||
actionType: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
requestedByType: {
|
||||
type: 'string',
|
||||
maxLength: 32,
|
||||
enum: [...SECURITY_SUBJECT_TYPES],
|
||||
},
|
||||
requestedAtMs: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
expiresAtMs: {
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
previewAvailable: { type: 'boolean' },
|
||||
preview: PREVIEW_SCHEMA,
|
||||
},
|
||||
required: [
|
||||
'requestId',
|
||||
'version',
|
||||
'state',
|
||||
'risk',
|
||||
'decisionMode',
|
||||
'permission',
|
||||
'actionType',
|
||||
'requestedByType',
|
||||
'requestedAtMs',
|
||||
'expiresAtMs',
|
||||
'previewAvailable',
|
||||
],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
required: ['found'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
effect: 'read',
|
||||
risk: 'low',
|
||||
requiredPermissions: ['approval.read', 'artifact.read'],
|
||||
timeoutSeconds: BUILTIN_APPROVAL_GET_TIMEOUT_SECONDS,
|
||||
});
|
||||
|
||||
export class InvalidBuiltInApprovalGetToolError extends TypeError {
|
||||
readonly code = 'BUILTIN_APPROVAL_GET_TOOL_INVALID';
|
||||
|
||||
constructor(message: string) {
|
||||
super(`Built-in Approval get Tool is invalid: ${message}`);
|
||||
this.name = 'InvalidBuiltInApprovalGetToolError';
|
||||
}
|
||||
}
|
||||
|
||||
export class BuiltInApprovalGetToolUnavailableError extends Error {
|
||||
readonly code = 'BUILTIN_APPROVAL_GET_TOOL_UNAVAILABLE';
|
||||
|
||||
constructor() {
|
||||
super('Built-in Approval get Tool is unavailable');
|
||||
this.name = 'BuiltInApprovalGetToolUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
function invalid(message: string): never {
|
||||
throw new InvalidBuiltInApprovalGetToolError(message);
|
||||
}
|
||||
|
||||
function boundedText(value: unknown, maximumBytes: number): value is string {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
value.length > 0 &&
|
||||
Buffer.byteLength(value, 'utf8') <= maximumBytes &&
|
||||
!CONTROL_PATTERN.test(value)
|
||||
);
|
||||
}
|
||||
|
||||
export async function executeBuiltInApprovalGetTool(
|
||||
source: Pick<ApprovalRequestDetailSource, 'getApprovalRequestDetail'>,
|
||||
projectId: string,
|
||||
input: ToolJsonValue,
|
||||
): Promise<Readonly<Record<string, ToolJsonValue>>> {
|
||||
const record =
|
||||
input && typeof input === 'object' && !Array.isArray(input)
|
||||
? (input as Readonly<Record<string, ToolJsonValue>>)
|
||||
: null;
|
||||
if (
|
||||
typeof source?.getApprovalRequestDetail !== 'function' ||
|
||||
!boundedText(projectId, 128) ||
|
||||
!record ||
|
||||
Reflect.ownKeys(record).length !== 1 ||
|
||||
!Object.hasOwn(record, 'requestId') ||
|
||||
!boundedText(record.requestId, 128)
|
||||
) {
|
||||
return invalid('execution context or input is invalid');
|
||||
}
|
||||
let detail;
|
||||
try {
|
||||
detail = await source.getApprovalRequestDetail({
|
||||
projectId,
|
||||
requestId: record.requestId,
|
||||
});
|
||||
} catch {
|
||||
throw new BuiltInApprovalGetToolUnavailableError();
|
||||
}
|
||||
if (!detail) return Object.freeze({ found: false });
|
||||
try {
|
||||
if (
|
||||
typeof detail !== 'object' ||
|
||||
Array.isArray(detail) ||
|
||||
Reflect.ownKeys(detail).length !== 2 ||
|
||||
!Object.hasOwn(detail, 'request') ||
|
||||
!Object.hasOwn(detail, 'preview')
|
||||
) {
|
||||
throw new BuiltInApprovalGetToolUnavailableError();
|
||||
}
|
||||
const request = normalizeApprovalRequestRecord(detail.request);
|
||||
const preview = detail.preview
|
||||
? normalizeApprovalDetailPreview(detail.preview)
|
||||
: null;
|
||||
const permission = normalizeProjectPermission(request.action.permission);
|
||||
if (
|
||||
request.projectId !== projectId ||
|
||||
request.id !== record.requestId ||
|
||||
(preview !== null && request.action.actionType !== 'tool.invoke')
|
||||
) {
|
||||
throw new BuiltInApprovalGetToolUnavailableError();
|
||||
}
|
||||
return Object.freeze({
|
||||
found: true,
|
||||
approval: Object.freeze({
|
||||
requestId: request.id,
|
||||
version: request.version,
|
||||
state: request.state,
|
||||
risk: request.risk,
|
||||
decisionMode: request.decisionMode,
|
||||
permission,
|
||||
actionType: request.action.actionType,
|
||||
requestedByType: request.requestedBy.type,
|
||||
requestedAtMs: request.requestedAtMs,
|
||||
expiresAtMs: request.expiresAtMs,
|
||||
previewAvailable: preview !== null,
|
||||
...(preview === null
|
||||
? {}
|
||||
: {
|
||||
preview: Object.freeze({
|
||||
title: preview.title,
|
||||
summary: preview.summary,
|
||||
fields: Object.freeze(
|
||||
preview.fields.map((field) =>
|
||||
Object.freeze({
|
||||
kind: field.kind,
|
||||
label: field.label,
|
||||
...(field.value === null ? {} : { value: field.value }),
|
||||
}),
|
||||
),
|
||||
),
|
||||
warnings: Object.freeze([...preview.warnings]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof BuiltInApprovalGetToolUnavailableError) throw error;
|
||||
throw new BuiltInApprovalGetToolUnavailableError();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
import {
|
||||
APPROVAL_DECISION_MODES,
|
||||
APPROVAL_REQUEST_STATES,
|
||||
APPROVAL_RISKS,
|
||||
normalizeApprovalRequestRecord,
|
||||
type ApprovalRequestRecord,
|
||||
} from '@qinglong/runtime-core/approved-action';
|
||||
import {
|
||||
approvalRequestUpdatedAtMs,
|
||||
type ApprovalRequestCursor,
|
||||
type ApprovalRequestSource,
|
||||
} from '@qinglong/runtime-core/approval-discovery';
|
||||
import { SECURITY_SUBJECT_TYPES } from '@qinglong/runtime-core/security';
|
||||
import { normalizeProjectPermission } from '@qinglong/runtime-core/project-policy';
|
||||
import {
|
||||
normalizeToolDefinition,
|
||||
type ToolJsonValue,
|
||||
} from '@qinglong/runtime-core/tool-registry';
|
||||
|
||||
export const BUILTIN_APPROVAL_LIST_TOOL = Object.freeze({
|
||||
name: 'qinglong.approval.list',
|
||||
version: '1.0.0',
|
||||
});
|
||||
export const BUILTIN_APPROVAL_LIST_TIMEOUT_SECONDS = 5;
|
||||
export const BUILTIN_APPROVAL_LIST_DEFAULT_LIMIT = 32;
|
||||
export const BUILTIN_APPROVAL_LIST_MAX_LIMIT = 64;
|
||||
|
||||
const MAX_INT = 2_147_483_647;
|
||||
const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
||||
|
||||
const APPROVAL_LIST_CURSOR_SCHEMA = Object.freeze({
|
||||
type: 'object',
|
||||
properties: {
|
||||
updatedAtMs: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
requestId: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
},
|
||||
required: ['updatedAtMs', 'requestId'],
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
export const BUILTIN_APPROVAL_LIST_TOOL_DEFINITION = normalizeToolDefinition({
|
||||
name: BUILTIN_APPROVAL_LIST_TOOL.name,
|
||||
version: BUILTIN_APPROVAL_LIST_TOOL.version,
|
||||
description:
|
||||
'List recent low-sensitive Approval requests in the authenticated Project',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
after: APPROVAL_LIST_CURSOR_SCHEMA,
|
||||
limit: {
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
maximum: BUILTIN_APPROVAL_LIST_MAX_LIMIT,
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
additionalProperties: false,
|
||||
},
|
||||
outputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
approvals: {
|
||||
type: 'array',
|
||||
maxItems: BUILTIN_APPROVAL_LIST_MAX_LIMIT,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
requestId: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
version: { type: 'integer', minimum: 1, maximum: MAX_INT },
|
||||
state: {
|
||||
type: 'string',
|
||||
maxLength: 32,
|
||||
enum: [...APPROVAL_REQUEST_STATES],
|
||||
},
|
||||
risk: {
|
||||
type: 'string',
|
||||
maxLength: 32,
|
||||
enum: [...APPROVAL_RISKS],
|
||||
},
|
||||
decisionMode: {
|
||||
type: 'string',
|
||||
maxLength: 32,
|
||||
enum: [...APPROVAL_DECISION_MODES],
|
||||
},
|
||||
permission: { type: 'string', minLength: 1, maxLength: 255 },
|
||||
actionType: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
requestedByType: {
|
||||
type: 'string',
|
||||
maxLength: 32,
|
||||
enum: [...SECURITY_SUBJECT_TYPES],
|
||||
},
|
||||
requestedAtMs: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
expiresAtMs: {
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
decidedAtMs: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
consumedAtMs: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
updatedAtMs: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'requestId',
|
||||
'version',
|
||||
'state',
|
||||
'risk',
|
||||
'decisionMode',
|
||||
'permission',
|
||||
'actionType',
|
||||
'requestedByType',
|
||||
'requestedAtMs',
|
||||
'expiresAtMs',
|
||||
'updatedAtMs',
|
||||
],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
hasMore: { type: 'boolean' },
|
||||
next: APPROVAL_LIST_CURSOR_SCHEMA,
|
||||
},
|
||||
required: ['approvals', 'hasMore'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
effect: 'read',
|
||||
risk: 'low',
|
||||
requiredPermissions: ['approval.read'],
|
||||
timeoutSeconds: BUILTIN_APPROVAL_LIST_TIMEOUT_SECONDS,
|
||||
});
|
||||
|
||||
export class InvalidBuiltInApprovalListToolError extends TypeError {
|
||||
readonly code = 'BUILTIN_APPROVAL_LIST_TOOL_INVALID';
|
||||
|
||||
constructor(message: string) {
|
||||
super(`Built-in Approval list Tool is invalid: ${message}`);
|
||||
this.name = 'InvalidBuiltInApprovalListToolError';
|
||||
}
|
||||
}
|
||||
|
||||
export class BuiltInApprovalListToolUnavailableError extends Error {
|
||||
readonly code = 'BUILTIN_APPROVAL_LIST_TOOL_UNAVAILABLE';
|
||||
|
||||
constructor() {
|
||||
super('Built-in Approval list Tool is unavailable');
|
||||
this.name = 'BuiltInApprovalListToolUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
function invalid(message: string): never {
|
||||
throw new InvalidBuiltInApprovalListToolError(message);
|
||||
}
|
||||
|
||||
function boundedText(value: unknown, maximumBytes: number): value is string {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
value.length > 0 &&
|
||||
Buffer.byteLength(value, 'utf8') <= maximumBytes &&
|
||||
!CONTROL_PATTERN.test(value)
|
||||
);
|
||||
}
|
||||
|
||||
function integer(
|
||||
value: unknown,
|
||||
minimum: number,
|
||||
maximum: number,
|
||||
): value is number {
|
||||
return (
|
||||
Number.isSafeInteger(value) &&
|
||||
Number(value) >= minimum &&
|
||||
Number(value) <= maximum
|
||||
);
|
||||
}
|
||||
|
||||
function cursor(value: ToolJsonValue | undefined): ApprovalRequestCursor | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return invalid('cursor is invalid');
|
||||
}
|
||||
const record = value as Readonly<Record<string, ToolJsonValue>>;
|
||||
if (
|
||||
Reflect.ownKeys(record).length !== 2 ||
|
||||
!Object.hasOwn(record, 'updatedAtMs') ||
|
||||
!Object.hasOwn(record, 'requestId') ||
|
||||
!integer(record.updatedAtMs, 0, Number.MAX_SAFE_INTEGER) ||
|
||||
!boundedText(record.requestId, 128)
|
||||
) {
|
||||
return invalid('cursor is invalid');
|
||||
}
|
||||
return Object.freeze({
|
||||
updatedAtMs: record.updatedAtMs,
|
||||
requestId: record.requestId,
|
||||
});
|
||||
}
|
||||
|
||||
function before(
|
||||
updatedAtMs: number,
|
||||
requestId: string,
|
||||
boundary?: Readonly<ApprovalRequestCursor>,
|
||||
): boolean {
|
||||
return (
|
||||
!boundary ||
|
||||
updatedAtMs < boundary.updatedAtMs ||
|
||||
(updatedAtMs === boundary.updatedAtMs && requestId < boundary.requestId)
|
||||
);
|
||||
}
|
||||
|
||||
function projectApproval(
|
||||
value: Readonly<ApprovalRequestRecord>,
|
||||
projectId: string,
|
||||
boundary?: Readonly<ApprovalRequestCursor>,
|
||||
): Readonly<Record<string, ToolJsonValue>> | null {
|
||||
let request: Readonly<ApprovalRequestRecord>;
|
||||
let permission: string;
|
||||
let updatedAtMs: number;
|
||||
try {
|
||||
request = normalizeApprovalRequestRecord(value);
|
||||
permission = normalizeProjectPermission(request.action.permission);
|
||||
updatedAtMs = approvalRequestUpdatedAtMs(request);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
request.projectId !== projectId ||
|
||||
!boundedText(request.id, 128) ||
|
||||
!before(updatedAtMs, request.id, boundary) ||
|
||||
!integer(request.version, 1, MAX_INT) ||
|
||||
!APPROVAL_REQUEST_STATES.includes(request.state) ||
|
||||
!APPROVAL_RISKS.includes(request.risk) ||
|
||||
!APPROVAL_DECISION_MODES.includes(request.decisionMode) ||
|
||||
!boundedText(permission, 255) ||
|
||||
!boundedText(request.action.actionType, 128) ||
|
||||
!SECURITY_SUBJECT_TYPES.includes(request.requestedBy.type) ||
|
||||
!integer(request.requestedAtMs, 0, Number.MAX_SAFE_INTEGER) ||
|
||||
!integer(request.expiresAtMs, 1, Number.MAX_SAFE_INTEGER) ||
|
||||
(request.decidedAtMs !== null &&
|
||||
!integer(request.decidedAtMs, 0, Number.MAX_SAFE_INTEGER)) ||
|
||||
(request.consumedAtMs !== null &&
|
||||
!integer(request.consumedAtMs, 0, Number.MAX_SAFE_INTEGER))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return Object.freeze({
|
||||
requestId: request.id,
|
||||
version: request.version,
|
||||
state: request.state,
|
||||
risk: request.risk,
|
||||
decisionMode: request.decisionMode,
|
||||
permission,
|
||||
actionType: request.action.actionType,
|
||||
requestedByType: request.requestedBy.type,
|
||||
requestedAtMs: request.requestedAtMs,
|
||||
expiresAtMs: request.expiresAtMs,
|
||||
...(request.decidedAtMs === null
|
||||
? {}
|
||||
: { decidedAtMs: request.decidedAtMs }),
|
||||
...(request.consumedAtMs === null
|
||||
? {}
|
||||
: { consumedAtMs: request.consumedAtMs }),
|
||||
updatedAtMs,
|
||||
});
|
||||
}
|
||||
|
||||
export async function executeBuiltInApprovalListTool(
|
||||
source: Pick<ApprovalRequestSource, 'listApprovalRequests'>,
|
||||
projectId: string,
|
||||
input: ToolJsonValue,
|
||||
): Promise<Readonly<Record<string, ToolJsonValue>>> {
|
||||
const record =
|
||||
input && typeof input === 'object' && !Array.isArray(input)
|
||||
? (input as Readonly<Record<string, ToolJsonValue>>)
|
||||
: null;
|
||||
const keys = record ? Reflect.ownKeys(record) : [];
|
||||
if (
|
||||
typeof source?.listApprovalRequests !== 'function' ||
|
||||
!boundedText(projectId, 128) ||
|
||||
!record ||
|
||||
keys.length > 2 ||
|
||||
keys.some((key) => key !== 'after' && key !== 'limit') ||
|
||||
(record.limit !== undefined &&
|
||||
!integer(record.limit, 1, BUILTIN_APPROVAL_LIST_MAX_LIMIT))
|
||||
) {
|
||||
return invalid('execution context or input is invalid');
|
||||
}
|
||||
const after = cursor(record.after);
|
||||
const limit = record.limit ?? BUILTIN_APPROVAL_LIST_DEFAULT_LIMIT;
|
||||
let page;
|
||||
try {
|
||||
page = await source.listApprovalRequests({
|
||||
projectId,
|
||||
limit,
|
||||
...(after ? { after } : {}),
|
||||
});
|
||||
} catch {
|
||||
throw new BuiltInApprovalListToolUnavailableError();
|
||||
}
|
||||
if (
|
||||
!page ||
|
||||
!Array.isArray(page.requests) ||
|
||||
page.requests.length > limit ||
|
||||
typeof page.truncated !== 'boolean' ||
|
||||
page.truncated !== Boolean(page.next)
|
||||
) {
|
||||
throw new BuiltInApprovalListToolUnavailableError();
|
||||
}
|
||||
const approvals: Readonly<Record<string, ToolJsonValue>>[] = [];
|
||||
let boundary = after;
|
||||
for (const request of page.requests) {
|
||||
const projected = projectApproval(request, projectId, boundary);
|
||||
if (!projected) throw new BuiltInApprovalListToolUnavailableError();
|
||||
approvals.push(projected);
|
||||
boundary = Object.freeze({
|
||||
updatedAtMs: projected.updatedAtMs as number,
|
||||
requestId: projected.requestId as string,
|
||||
});
|
||||
}
|
||||
if (
|
||||
page.truncated &&
|
||||
(!page.next ||
|
||||
Reflect.ownKeys(page.next).length !== 2 ||
|
||||
!boundary ||
|
||||
page.next.updatedAtMs !== boundary.updatedAtMs ||
|
||||
page.next.requestId !== boundary.requestId ||
|
||||
approvals.length === 0)
|
||||
) {
|
||||
throw new BuiltInApprovalListToolUnavailableError();
|
||||
}
|
||||
return Object.freeze({
|
||||
approvals: Object.freeze(approvals),
|
||||
hasMore: page.truncated,
|
||||
...(page.truncated ? { next: Object.freeze({ ...boundary! }) } : {}),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import { RUN_EVENT_ACTOR_TYPES } from '@qinglong/runtime-core/run';
|
||||
import type { RunRepositoryReader } from '@qinglong/runtime-core/run-repository';
|
||||
import {
|
||||
BoundedRunEventListProjectionUnavailableError,
|
||||
DEFAULT_BOUNDED_RUN_EVENT_LIST_LIMIT,
|
||||
InvalidBoundedRunEventListProjectionError,
|
||||
MAX_BOUNDED_RUN_EVENT_LIST_LIMIT,
|
||||
executeBoundedRunEventListProjection,
|
||||
} from '@qinglong/runtime-core/bounded-run-event-list-projection';
|
||||
import {
|
||||
normalizeToolDefinition,
|
||||
type ToolJsonValue,
|
||||
} from '@qinglong/runtime-core/tool-registry';
|
||||
|
||||
export const BUILTIN_RUN_EVENT_LIST_TOOL = Object.freeze({
|
||||
name: 'qinglong.run.events.list',
|
||||
version: '1.0.0',
|
||||
});
|
||||
export const BUILTIN_RUN_EVENT_LIST_TIMEOUT_SECONDS = 5;
|
||||
export const BUILTIN_RUN_EVENT_LIST_DEFAULT_LIMIT =
|
||||
DEFAULT_BOUNDED_RUN_EVENT_LIST_LIMIT;
|
||||
export const BUILTIN_RUN_EVENT_LIST_MAX_LIMIT =
|
||||
MAX_BOUNDED_RUN_EVENT_LIST_LIMIT;
|
||||
|
||||
const MAX_INT = 2_147_483_647;
|
||||
|
||||
export const BUILTIN_RUN_EVENT_LIST_TOOL_DEFINITION = normalizeToolDefinition({
|
||||
name: BUILTIN_RUN_EVENT_LIST_TOOL.name,
|
||||
version: BUILTIN_RUN_EVENT_LIST_TOOL.version,
|
||||
description: 'List bounded low-sensitive events for one Project-scoped Run',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
runId: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
afterSequence: { type: 'integer', minimum: 0, maximum: MAX_INT },
|
||||
limit: {
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
maximum: BUILTIN_RUN_EVENT_LIST_MAX_LIMIT,
|
||||
},
|
||||
},
|
||||
required: ['runId'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
outputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
found: { type: 'boolean' },
|
||||
events: {
|
||||
type: 'array',
|
||||
maxItems: BUILTIN_RUN_EVENT_LIST_MAX_LIMIT,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sequence: { type: 'integer', minimum: 0, maximum: MAX_INT },
|
||||
type: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
actorType: {
|
||||
type: 'string',
|
||||
maxLength: 32,
|
||||
enum: RUN_EVENT_ACTOR_TYPES,
|
||||
},
|
||||
createdAtMs: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
},
|
||||
required: ['sequence', 'type', 'actorType', 'createdAtMs'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
hasMore: { type: 'boolean' },
|
||||
nextAfterSequence: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: MAX_INT,
|
||||
},
|
||||
},
|
||||
required: ['found', 'events', 'hasMore', 'nextAfterSequence'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
effect: 'read',
|
||||
risk: 'low',
|
||||
requiredPermissions: ['run.read'],
|
||||
timeoutSeconds: BUILTIN_RUN_EVENT_LIST_TIMEOUT_SECONDS,
|
||||
});
|
||||
|
||||
export class InvalidBuiltInRunEventListToolError extends TypeError {
|
||||
readonly code = 'BUILTIN_RUN_EVENT_LIST_TOOL_INVALID';
|
||||
|
||||
constructor(message: string) {
|
||||
super(`Built-in Run event list Tool is invalid: ${message}`);
|
||||
this.name = 'InvalidBuiltInRunEventListToolError';
|
||||
}
|
||||
}
|
||||
|
||||
export class BuiltInRunEventListToolUnavailableError extends Error {
|
||||
readonly code = 'BUILTIN_RUN_EVENT_LIST_TOOL_UNAVAILABLE';
|
||||
|
||||
constructor() {
|
||||
super('Built-in Run event list Tool is unavailable');
|
||||
this.name = 'BuiltInRunEventListToolUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
||||
|
||||
function invalid(message: string): never {
|
||||
throw new InvalidBuiltInRunEventListToolError(message);
|
||||
}
|
||||
|
||||
function boundedText(value: unknown, maximum: number): value is string {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
value.length > 0 &&
|
||||
value.length <= maximum &&
|
||||
!CONTROL_PATTERN.test(value)
|
||||
);
|
||||
}
|
||||
|
||||
function integer(
|
||||
value: unknown,
|
||||
minimum: number,
|
||||
maximum: number,
|
||||
): value is number {
|
||||
return (
|
||||
Number.isSafeInteger(value) &&
|
||||
Number(value) >= minimum &&
|
||||
Number(value) <= maximum
|
||||
);
|
||||
}
|
||||
|
||||
export async function executeBuiltInRunEventListTool(
|
||||
runs: Pick<RunRepositoryReader, 'findRunById' | 'listEvents'>,
|
||||
projectId: string,
|
||||
input: ToolJsonValue,
|
||||
): Promise<Readonly<Record<string, ToolJsonValue>>> {
|
||||
const inputRecord =
|
||||
input && typeof input === 'object' && !Array.isArray(input)
|
||||
? (input as Readonly<Record<string, ToolJsonValue>>)
|
||||
: null;
|
||||
const inputKeys = inputRecord ? Reflect.ownKeys(inputRecord) : [];
|
||||
if (
|
||||
!runs ||
|
||||
typeof runs.findRunById !== 'function' ||
|
||||
typeof runs.listEvents !== 'function' ||
|
||||
!boundedText(projectId, 128) ||
|
||||
!inputRecord ||
|
||||
inputKeys.length < 1 ||
|
||||
inputKeys.length > 3 ||
|
||||
inputKeys.some(
|
||||
(key) => key !== 'runId' && key !== 'afterSequence' && key !== 'limit',
|
||||
) ||
|
||||
!Object.hasOwn(inputRecord, 'runId') ||
|
||||
!boundedText(inputRecord.runId, 128) ||
|
||||
(inputRecord.afterSequence !== undefined &&
|
||||
!integer(inputRecord.afterSequence, 0, MAX_INT)) ||
|
||||
(inputRecord.limit !== undefined &&
|
||||
!integer(inputRecord.limit, 1, BUILTIN_RUN_EVENT_LIST_MAX_LIMIT))
|
||||
) {
|
||||
return invalid('execution context or input is invalid');
|
||||
}
|
||||
const runId = inputRecord.runId;
|
||||
const afterSequence = inputRecord.afterSequence ?? 0;
|
||||
const limit = inputRecord.limit ?? BUILTIN_RUN_EVENT_LIST_DEFAULT_LIMIT;
|
||||
try {
|
||||
const projection = await executeBoundedRunEventListProjection(
|
||||
runs,
|
||||
projectId,
|
||||
runId,
|
||||
{ afterSequence, limit },
|
||||
);
|
||||
return Object.freeze({
|
||||
found: projection.found,
|
||||
events: Object.freeze(
|
||||
projection.events.map((event) =>
|
||||
Object.freeze({
|
||||
sequence: event.sequence,
|
||||
type: event.type,
|
||||
actorType: event.actorType,
|
||||
createdAtMs: event.createdAtMs,
|
||||
}),
|
||||
),
|
||||
),
|
||||
hasMore: projection.hasMore,
|
||||
nextAfterSequence: projection.nextAfterSequence,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidBoundedRunEventListProjectionError) {
|
||||
return invalid('execution context or input is invalid');
|
||||
}
|
||||
if (error instanceof BoundedRunEventListProjectionUnavailableError) {
|
||||
throw new BuiltInRunEventListToolUnavailableError();
|
||||
}
|
||||
throw new BuiltInRunEventListToolUnavailableError();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
import {
|
||||
EXECUTION_ORIGINS,
|
||||
RUN_STATUSES,
|
||||
} from '@qinglong/runtime-core/run';
|
||||
import {
|
||||
type ProjectRunListCursor,
|
||||
type ProjectRunListReader,
|
||||
} from '@qinglong/runtime-core/project-run-list';
|
||||
import {
|
||||
BoundedRunListProjectionUnavailableError,
|
||||
DEFAULT_BOUNDED_RUN_LIST_LIMIT,
|
||||
InvalidBoundedRunListProjectionError,
|
||||
MAX_BOUNDED_RUN_LIST_LIMIT,
|
||||
executeBoundedRunListProjection,
|
||||
} from '@qinglong/runtime-core/bounded-run-list-projection';
|
||||
import {
|
||||
normalizeToolDefinition,
|
||||
type ToolJsonValue,
|
||||
} from '@qinglong/runtime-core/tool-registry';
|
||||
|
||||
export const BUILTIN_RUN_LIST_TOOL = Object.freeze({
|
||||
name: 'qinglong.run.list',
|
||||
version: '1.0.0',
|
||||
});
|
||||
export const BUILTIN_RUN_LIST_TIMEOUT_SECONDS = 5;
|
||||
export const BUILTIN_RUN_LIST_DEFAULT_LIMIT = DEFAULT_BOUNDED_RUN_LIST_LIMIT;
|
||||
export const BUILTIN_RUN_LIST_MAX_LIMIT = MAX_BOUNDED_RUN_LIST_LIMIT;
|
||||
|
||||
const MAX_INT = 2_147_483_647;
|
||||
const MIN_INT = -2_147_483_648;
|
||||
const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
||||
|
||||
const RUN_LIST_ITEM_SCHEMA = Object.freeze({
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
taskId: { type: 'string', minLength: 1, maxLength: 255 },
|
||||
taskRevision: { type: 'string', minLength: 1, maxLength: 255 },
|
||||
status: { type: 'string', maxLength: 32, enum: RUN_STATUSES },
|
||||
version: { type: 'integer', minimum: 0, maximum: MAX_INT },
|
||||
eventSequence: { type: 'integer', minimum: 0, maximum: MAX_INT },
|
||||
priority: { type: 'integer', minimum: MIN_INT, maximum: MAX_INT },
|
||||
executionOrigin: {
|
||||
type: 'string',
|
||||
maxLength: 32,
|
||||
enum: EXECUTION_ORIGINS,
|
||||
},
|
||||
executionOwner: {
|
||||
type: 'string',
|
||||
maxLength: 16,
|
||||
enum: ['legacy', 'runtime'],
|
||||
},
|
||||
createdAtMs: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
queuedAtMs: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
startedAtMs: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
finishedAtMs: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'id',
|
||||
'taskId',
|
||||
'taskRevision',
|
||||
'status',
|
||||
'version',
|
||||
'eventSequence',
|
||||
'priority',
|
||||
'executionOrigin',
|
||||
'executionOwner',
|
||||
'createdAtMs',
|
||||
],
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
const RUN_LIST_CURSOR_SCHEMA = Object.freeze({
|
||||
type: 'object',
|
||||
properties: {
|
||||
createdAtMs: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
runId: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
},
|
||||
required: ['createdAtMs', 'runId'],
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
export const BUILTIN_RUN_LIST_TOOL_DEFINITION = normalizeToolDefinition({
|
||||
name: BUILTIN_RUN_LIST_TOOL.name,
|
||||
version: BUILTIN_RUN_LIST_TOOL.version,
|
||||
description: 'List recent low-sensitive Runs in the authenticated Project',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
after: RUN_LIST_CURSOR_SCHEMA,
|
||||
limit: {
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
maximum: BUILTIN_RUN_LIST_MAX_LIMIT,
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
additionalProperties: false,
|
||||
},
|
||||
outputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
runs: {
|
||||
type: 'array',
|
||||
maxItems: BUILTIN_RUN_LIST_MAX_LIMIT,
|
||||
items: RUN_LIST_ITEM_SCHEMA,
|
||||
},
|
||||
hasMore: { type: 'boolean' },
|
||||
next: RUN_LIST_CURSOR_SCHEMA,
|
||||
},
|
||||
required: ['runs', 'hasMore'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
effect: 'read',
|
||||
risk: 'low',
|
||||
requiredPermissions: ['run.read'],
|
||||
timeoutSeconds: BUILTIN_RUN_LIST_TIMEOUT_SECONDS,
|
||||
});
|
||||
|
||||
export class InvalidBuiltInRunListToolError extends TypeError {
|
||||
readonly code = 'BUILTIN_RUN_LIST_TOOL_INVALID';
|
||||
|
||||
constructor(message: string) {
|
||||
super(`Built-in Run list Tool is invalid: ${message}`);
|
||||
this.name = 'InvalidBuiltInRunListToolError';
|
||||
}
|
||||
}
|
||||
|
||||
export class BuiltInRunListToolUnavailableError extends Error {
|
||||
readonly code = 'BUILTIN_RUN_LIST_TOOL_UNAVAILABLE';
|
||||
|
||||
constructor() {
|
||||
super('Built-in Run list Tool is unavailable');
|
||||
this.name = 'BuiltInRunListToolUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
function invalid(message: string): never {
|
||||
throw new InvalidBuiltInRunListToolError(message);
|
||||
}
|
||||
|
||||
function boundedText(value: unknown, maximum: number): value is string {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
value.length > 0 &&
|
||||
value.length <= maximum &&
|
||||
!CONTROL_PATTERN.test(value)
|
||||
);
|
||||
}
|
||||
|
||||
function integer(
|
||||
value: unknown,
|
||||
minimum: number,
|
||||
maximum: number,
|
||||
): value is number {
|
||||
return (
|
||||
Number.isSafeInteger(value) &&
|
||||
Number(value) >= minimum &&
|
||||
Number(value) <= maximum
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeCursor(
|
||||
value: ToolJsonValue | undefined,
|
||||
): ProjectRunListCursor | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return invalid('cursor is invalid');
|
||||
}
|
||||
const cursor = value as Readonly<Record<string, ToolJsonValue>>;
|
||||
if (
|
||||
Reflect.ownKeys(cursor).length !== 2 ||
|
||||
!Object.hasOwn(cursor, 'createdAtMs') ||
|
||||
!Object.hasOwn(cursor, 'runId') ||
|
||||
!integer(cursor.createdAtMs, 0, Number.MAX_SAFE_INTEGER) ||
|
||||
!boundedText(cursor.runId, 128)
|
||||
) {
|
||||
return invalid('cursor is invalid');
|
||||
}
|
||||
return Object.freeze({
|
||||
createdAtMs: cursor.createdAtMs,
|
||||
runId: cursor.runId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function executeBuiltInRunListTool(
|
||||
runs: ProjectRunListReader,
|
||||
projectId: string,
|
||||
input: ToolJsonValue,
|
||||
): Promise<Readonly<Record<string, ToolJsonValue>>> {
|
||||
const inputRecord =
|
||||
input && typeof input === 'object' && !Array.isArray(input)
|
||||
? (input as Readonly<Record<string, ToolJsonValue>>)
|
||||
: null;
|
||||
const inputKeys = inputRecord ? Reflect.ownKeys(inputRecord) : [];
|
||||
if (
|
||||
!runs ||
|
||||
typeof runs.listRunsByProject !== 'function' ||
|
||||
!boundedText(projectId, 128) ||
|
||||
!inputRecord ||
|
||||
inputKeys.length > 2 ||
|
||||
inputKeys.some((key) => key !== 'after' && key !== 'limit') ||
|
||||
(inputRecord.limit !== undefined &&
|
||||
!integer(inputRecord.limit, 1, BUILTIN_RUN_LIST_MAX_LIMIT))
|
||||
) {
|
||||
return invalid('execution context or input is invalid');
|
||||
}
|
||||
const after = normalizeCursor(inputRecord.after);
|
||||
const limit = inputRecord.limit ?? BUILTIN_RUN_LIST_DEFAULT_LIMIT;
|
||||
let result;
|
||||
try {
|
||||
result = await executeBoundedRunListProjection(runs, projectId, {
|
||||
limit,
|
||||
...(after === undefined ? {} : { after }),
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidBoundedRunListProjectionError) {
|
||||
return invalid('execution context or input is invalid');
|
||||
}
|
||||
if (!(error instanceof BoundedRunListProjectionUnavailableError)) {
|
||||
throw error;
|
||||
}
|
||||
throw new BuiltInRunListToolUnavailableError();
|
||||
}
|
||||
const projected: readonly ToolJsonValue[] = result.runs.map((run) =>
|
||||
Object.freeze({ ...run }),
|
||||
);
|
||||
return Object.freeze({
|
||||
runs: Object.freeze(projected),
|
||||
hasMore: result.hasMore,
|
||||
...(result.next === undefined
|
||||
? {}
|
||||
: {
|
||||
next: Object.freeze({
|
||||
createdAtMs: result.next.createdAtMs,
|
||||
runId: result.next.runId,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
import {
|
||||
BoundedRunStepListProjectionUnavailableError,
|
||||
DEFAULT_BOUNDED_RUN_STEP_LIST_LIMIT,
|
||||
InvalidBoundedRunStepListProjectionError,
|
||||
MAX_BOUNDED_RUN_STEP_LIST_LIMIT,
|
||||
executeBoundedRunStepListProjection,
|
||||
} from '@qinglong/runtime-core/bounded-run-step-list-projection';
|
||||
import type { RunRepositoryReader } from '@qinglong/runtime-core/run-repository';
|
||||
import {
|
||||
MAX_STEP_RUN_ATTEMPTS,
|
||||
STEP_RUN_KINDS,
|
||||
STEP_RUN_STATUSES,
|
||||
type StepRunRepository,
|
||||
} from '@qinglong/runtime-core/step-run';
|
||||
import {
|
||||
normalizeToolDefinition,
|
||||
type ToolJsonValue,
|
||||
} from '@qinglong/runtime-core/tool-registry';
|
||||
|
||||
export const BUILTIN_RUN_STEP_LIST_TOOL = Object.freeze({
|
||||
name: 'qinglong.run.steps.list',
|
||||
version: '1.0.0',
|
||||
});
|
||||
export const BUILTIN_RUN_STEP_LIST_TIMEOUT_SECONDS = 5;
|
||||
export const BUILTIN_RUN_STEP_LIST_DEFAULT_LIMIT =
|
||||
DEFAULT_BOUNDED_RUN_STEP_LIST_LIMIT;
|
||||
export const BUILTIN_RUN_STEP_LIST_MAX_LIMIT = MAX_BOUNDED_RUN_STEP_LIST_LIMIT;
|
||||
|
||||
const MAX_INT = 2_147_483_647;
|
||||
const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
||||
|
||||
export const BUILTIN_RUN_STEP_LIST_TOOL_DEFINITION = normalizeToolDefinition({
|
||||
name: BUILTIN_RUN_STEP_LIST_TOOL.name,
|
||||
version: BUILTIN_RUN_STEP_LIST_TOOL.version,
|
||||
description: 'List bounded low-sensitive Steps for one Project-scoped Run',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
runId: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
afterStepKey: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
afterStepRunId: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
limit: {
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
maximum: BUILTIN_RUN_STEP_LIST_MAX_LIMIT,
|
||||
},
|
||||
},
|
||||
required: ['runId'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
outputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
found: { type: 'boolean' },
|
||||
steps: {
|
||||
type: 'array',
|
||||
maxItems: BUILTIN_RUN_STEP_LIST_MAX_LIMIT,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
parentStepRunId: {
|
||||
type: 'string',
|
||||
minLength: 1,
|
||||
maxLength: 128,
|
||||
},
|
||||
stepKey: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
kind: {
|
||||
type: 'string',
|
||||
minLength: 1,
|
||||
maxLength: 32,
|
||||
enum: STEP_RUN_KINDS,
|
||||
},
|
||||
required: { type: 'boolean' },
|
||||
status: {
|
||||
type: 'string',
|
||||
minLength: 1,
|
||||
maxLength: 32,
|
||||
enum: STEP_RUN_STATUSES,
|
||||
},
|
||||
version: { type: 'integer', minimum: 1, maximum: MAX_INT },
|
||||
attemptCount: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: MAX_STEP_RUN_ATTEMPTS,
|
||||
},
|
||||
readyAtMs: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
startedAtMs: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
finishedAtMs: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
resultCode: { type: 'string', minLength: 1, maxLength: 64 },
|
||||
createdAtMs: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
updatedAtMs: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'id',
|
||||
'stepKey',
|
||||
'kind',
|
||||
'required',
|
||||
'status',
|
||||
'version',
|
||||
'attemptCount',
|
||||
'createdAtMs',
|
||||
'updatedAtMs',
|
||||
],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
hasMore: { type: 'boolean' },
|
||||
next: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
stepKey: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
stepRunId: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
},
|
||||
required: ['stepKey', 'stepRunId'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
required: ['found', 'steps', 'hasMore'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
effect: 'read',
|
||||
risk: 'low',
|
||||
requiredPermissions: ['run.read'],
|
||||
timeoutSeconds: BUILTIN_RUN_STEP_LIST_TIMEOUT_SECONDS,
|
||||
});
|
||||
|
||||
export class InvalidBuiltInRunStepListToolError extends TypeError {
|
||||
readonly code = 'BUILTIN_RUN_STEP_LIST_TOOL_INVALID';
|
||||
|
||||
constructor(message: string) {
|
||||
super(`Built-in Run Step list Tool is invalid: ${message}`);
|
||||
this.name = 'InvalidBuiltInRunStepListToolError';
|
||||
}
|
||||
}
|
||||
|
||||
export class BuiltInRunStepListToolUnavailableError extends Error {
|
||||
readonly code = 'BUILTIN_RUN_STEP_LIST_TOOL_UNAVAILABLE';
|
||||
|
||||
constructor() {
|
||||
super('Built-in Run Step list Tool is unavailable');
|
||||
this.name = 'BuiltInRunStepListToolUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
function boundedText(value: unknown, maximum: number): value is string {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
value.length > 0 &&
|
||||
value.length <= maximum &&
|
||||
!CONTROL_PATTERN.test(value)
|
||||
);
|
||||
}
|
||||
|
||||
function invalid(message: string): never {
|
||||
throw new InvalidBuiltInRunStepListToolError(message);
|
||||
}
|
||||
|
||||
export async function executeBuiltInRunStepListTool(
|
||||
runs: Pick<RunRepositoryReader, 'findRunById'>,
|
||||
stepRuns: Pick<StepRunRepository, 'listByRun'>,
|
||||
projectId: string,
|
||||
input: ToolJsonValue,
|
||||
): Promise<Readonly<Record<string, ToolJsonValue>>> {
|
||||
const value =
|
||||
input && typeof input === 'object' && !Array.isArray(input)
|
||||
? (input as Readonly<Record<string, ToolJsonValue>>)
|
||||
: null;
|
||||
const keys = value ? Reflect.ownKeys(value) : [];
|
||||
const afterStepKey = value?.afterStepKey;
|
||||
const afterStepRunId = value?.afterStepRunId;
|
||||
if (
|
||||
!runs ||
|
||||
typeof runs.findRunById !== 'function' ||
|
||||
!stepRuns ||
|
||||
typeof stepRuns.listByRun !== 'function' ||
|
||||
!boundedText(projectId, 128) ||
|
||||
!value ||
|
||||
keys.length < 1 ||
|
||||
keys.length > 4 ||
|
||||
keys.some(
|
||||
(key) =>
|
||||
key !== 'runId' &&
|
||||
key !== 'afterStepKey' &&
|
||||
key !== 'afterStepRunId' &&
|
||||
key !== 'limit',
|
||||
) ||
|
||||
!Object.hasOwn(value, 'runId') ||
|
||||
!boundedText(value.runId, 128) ||
|
||||
(afterStepKey === undefined) !== (afterStepRunId === undefined) ||
|
||||
(afterStepKey !== undefined && !boundedText(afterStepKey, 128)) ||
|
||||
(afterStepRunId !== undefined && !boundedText(afterStepRunId, 128)) ||
|
||||
(value.limit !== undefined &&
|
||||
(!Number.isSafeInteger(value.limit) ||
|
||||
Number(value.limit) < 1 ||
|
||||
Number(value.limit) > BUILTIN_RUN_STEP_LIST_MAX_LIMIT))
|
||||
) {
|
||||
return invalid('execution context or input is invalid');
|
||||
}
|
||||
try {
|
||||
const projection = await executeBoundedRunStepListProjection(
|
||||
runs,
|
||||
stepRuns,
|
||||
projectId,
|
||||
value.runId,
|
||||
{
|
||||
limit: Number(value.limit ?? BUILTIN_RUN_STEP_LIST_DEFAULT_LIMIT),
|
||||
...(afterStepKey === undefined || afterStepRunId === undefined
|
||||
? {}
|
||||
: { after: { stepKey: afterStepKey, stepRunId: afterStepRunId } }),
|
||||
},
|
||||
);
|
||||
return Object.freeze({
|
||||
found: projection.found,
|
||||
steps: Object.freeze(
|
||||
projection.steps.map((step) =>
|
||||
Object.freeze({
|
||||
id: step.id,
|
||||
...(step.parentStepRunId === null
|
||||
? {}
|
||||
: { parentStepRunId: step.parentStepRunId }),
|
||||
stepKey: step.stepKey,
|
||||
kind: step.kind,
|
||||
required: step.required,
|
||||
status: step.status,
|
||||
version: step.version,
|
||||
attemptCount: step.attemptCount,
|
||||
...(step.readyAtMs === null ? {} : { readyAtMs: step.readyAtMs }),
|
||||
...(step.startedAtMs === null
|
||||
? {}
|
||||
: { startedAtMs: step.startedAtMs }),
|
||||
...(step.finishedAtMs === null
|
||||
? {}
|
||||
: { finishedAtMs: step.finishedAtMs }),
|
||||
...(step.resultCode === null
|
||||
? {}
|
||||
: { resultCode: step.resultCode }),
|
||||
createdAtMs: step.createdAtMs,
|
||||
updatedAtMs: step.updatedAtMs,
|
||||
}),
|
||||
),
|
||||
),
|
||||
hasMore: projection.hasMore,
|
||||
...(projection.next === null ? {} : { next: projection.next }),
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidBoundedRunStepListProjectionError) {
|
||||
return invalid('execution context or input is invalid');
|
||||
}
|
||||
if (error instanceof BoundedRunStepListProjectionUnavailableError) {
|
||||
throw new BuiltInRunStepListToolUnavailableError();
|
||||
}
|
||||
throw new BuiltInRunStepListToolUnavailableError();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
TASK_DEFINITION_KINDS,
|
||||
type TaskDefinitionSource,
|
||||
} from '@qinglong/runtime-core/task-definition';
|
||||
import {
|
||||
BoundedTaskReadProjectionUnavailableError,
|
||||
InvalidBoundedTaskReadProjectionError,
|
||||
executeBoundedTaskReadProjection,
|
||||
} from '@qinglong/runtime-core/bounded-task-read-projection';
|
||||
import {
|
||||
normalizeToolDefinition,
|
||||
type ToolJsonValue,
|
||||
} from '@qinglong/runtime-core/tool-registry';
|
||||
|
||||
export const BUILTIN_TASK_GET_TOOL = Object.freeze({
|
||||
name: 'qinglong.task.get',
|
||||
version: '1.0.0',
|
||||
});
|
||||
export const BUILTIN_TASK_GET_TIMEOUT_SECONDS = 5;
|
||||
|
||||
export const BUILTIN_TASK_GET_TOOL_DEFINITION = normalizeToolDefinition({
|
||||
name: BUILTIN_TASK_GET_TOOL.name,
|
||||
version: BUILTIN_TASK_GET_TOOL.version,
|
||||
description: 'Read one current low-sensitive Task and its immutable fence',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
taskId: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
},
|
||||
required: ['taskId'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
outputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
found: { type: 'boolean' },
|
||||
taskId: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
revision: { type: 'integer', minimum: 1, maximum: 2_147_483_647 },
|
||||
name: { type: 'string', minLength: 1, maxLength: 255 },
|
||||
kind: { type: 'string', maxLength: 16, enum: TASK_DEFINITION_KINDS },
|
||||
specSchema: { type: 'string', minLength: 1, maxLength: 137 },
|
||||
enabled: { type: 'boolean' },
|
||||
contentDigest: { type: 'string', minLength: 64, maxLength: 64 },
|
||||
createdAtMs: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
updatedAtMs: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
},
|
||||
required: ['found'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
effect: 'read',
|
||||
risk: 'low',
|
||||
requiredPermissions: ['task.read'],
|
||||
timeoutSeconds: BUILTIN_TASK_GET_TIMEOUT_SECONDS,
|
||||
});
|
||||
|
||||
export class InvalidBuiltInTaskGetToolError extends TypeError {
|
||||
readonly code = 'BUILTIN_TASK_GET_TOOL_INVALID';
|
||||
|
||||
constructor() {
|
||||
super('Built-in Task get Tool input is invalid');
|
||||
this.name = 'InvalidBuiltInTaskGetToolError';
|
||||
}
|
||||
}
|
||||
|
||||
export class BuiltInTaskGetToolUnavailableError extends Error {
|
||||
readonly code = 'BUILTIN_TASK_GET_TOOL_UNAVAILABLE';
|
||||
|
||||
constructor() {
|
||||
super('Built-in Task get Tool is unavailable');
|
||||
this.name = 'BuiltInTaskGetToolUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeBuiltInTaskGetTool(
|
||||
source: Pick<TaskDefinitionSource, 'findCurrentTaskDefinition'>,
|
||||
projectId: string,
|
||||
input: ToolJsonValue,
|
||||
): Promise<Readonly<Record<string, ToolJsonValue>>> {
|
||||
const record =
|
||||
input && typeof input === 'object' && !Array.isArray(input)
|
||||
? (input as Readonly<Record<string, ToolJsonValue>>)
|
||||
: null;
|
||||
if (
|
||||
!record ||
|
||||
Reflect.ownKeys(record).length !== 1 ||
|
||||
!Object.hasOwn(record, 'taskId') ||
|
||||
typeof record.taskId !== 'string'
|
||||
) {
|
||||
throw new InvalidBuiltInTaskGetToolError();
|
||||
}
|
||||
try {
|
||||
return await executeBoundedTaskReadProjection(
|
||||
source,
|
||||
projectId,
|
||||
record.taskId,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidBoundedTaskReadProjectionError) {
|
||||
throw new InvalidBuiltInTaskGetToolError();
|
||||
}
|
||||
if (error instanceof BoundedTaskReadProjectionUnavailableError) {
|
||||
throw new BuiltInTaskGetToolUnavailableError();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import {
|
||||
TASK_DEFINITION_KINDS,
|
||||
type TaskDefinitionSource,
|
||||
} from '@qinglong/runtime-core/task-definition';
|
||||
import {
|
||||
BoundedTaskListProjectionUnavailableError,
|
||||
DEFAULT_BOUNDED_TASK_LIST_LIMIT,
|
||||
InvalidBoundedTaskListProjectionError,
|
||||
MAX_BOUNDED_TASK_LIST_LIMIT,
|
||||
executeBoundedTaskListProjection,
|
||||
} from '@qinglong/runtime-core/bounded-task-list-projection';
|
||||
import {
|
||||
normalizeToolDefinition,
|
||||
type ToolJsonValue,
|
||||
} from '@qinglong/runtime-core/tool-registry';
|
||||
|
||||
export const BUILTIN_TASK_LIST_TOOL = Object.freeze({
|
||||
name: 'qinglong.task.list',
|
||||
version: '1.0.0',
|
||||
});
|
||||
export const BUILTIN_TASK_LIST_TIMEOUT_SECONDS = 5;
|
||||
export const BUILTIN_TASK_LIST_DEFAULT_LIMIT = DEFAULT_BOUNDED_TASK_LIST_LIMIT;
|
||||
export const BUILTIN_TASK_LIST_MAX_LIMIT = MAX_BOUNDED_TASK_LIST_LIMIT;
|
||||
|
||||
const TASK_LIST_CURSOR_SCHEMA = Object.freeze({
|
||||
type: 'object',
|
||||
properties: {
|
||||
taskId: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
},
|
||||
required: ['taskId'],
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
export const BUILTIN_TASK_LIST_TOOL_DEFINITION = normalizeToolDefinition({
|
||||
name: BUILTIN_TASK_LIST_TOOL.name,
|
||||
version: BUILTIN_TASK_LIST_TOOL.version,
|
||||
description: 'List current low-sensitive Tasks in the authenticated Project',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
after: TASK_LIST_CURSOR_SCHEMA,
|
||||
limit: {
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
maximum: BUILTIN_TASK_LIST_MAX_LIMIT,
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
additionalProperties: false,
|
||||
},
|
||||
outputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tasks: {
|
||||
type: 'array',
|
||||
maxItems: BUILTIN_TASK_LIST_MAX_LIMIT,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
taskId: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
revision: { type: 'integer', minimum: 1, maximum: 2_147_483_647 },
|
||||
name: { type: 'string', minLength: 1, maxLength: 255 },
|
||||
kind: {
|
||||
type: 'string',
|
||||
maxLength: 16,
|
||||
enum: TASK_DEFINITION_KINDS,
|
||||
},
|
||||
specSchema: { type: 'string', minLength: 1, maxLength: 137 },
|
||||
enabled: { type: 'boolean' },
|
||||
updatedAtMs: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'taskId',
|
||||
'revision',
|
||||
'name',
|
||||
'kind',
|
||||
'specSchema',
|
||||
'enabled',
|
||||
'updatedAtMs',
|
||||
],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
hasMore: { type: 'boolean' },
|
||||
next: TASK_LIST_CURSOR_SCHEMA,
|
||||
},
|
||||
required: ['tasks', 'hasMore'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
effect: 'read',
|
||||
risk: 'low',
|
||||
requiredPermissions: ['task.read'],
|
||||
timeoutSeconds: BUILTIN_TASK_LIST_TIMEOUT_SECONDS,
|
||||
});
|
||||
|
||||
export class InvalidBuiltInTaskListToolError extends TypeError {
|
||||
readonly code = 'BUILTIN_TASK_LIST_TOOL_INVALID';
|
||||
|
||||
constructor(message: string) {
|
||||
super(`Built-in Task list Tool is invalid: ${message}`);
|
||||
this.name = 'InvalidBuiltInTaskListToolError';
|
||||
}
|
||||
}
|
||||
|
||||
export class BuiltInTaskListToolUnavailableError extends Error {
|
||||
readonly code = 'BUILTIN_TASK_LIST_TOOL_UNAVAILABLE';
|
||||
|
||||
constructor() {
|
||||
super('Built-in Task list Tool is unavailable');
|
||||
this.name = 'BuiltInTaskListToolUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
function invalid(message: string): never {
|
||||
throw new InvalidBuiltInTaskListToolError(message);
|
||||
}
|
||||
|
||||
export async function executeBuiltInTaskListTool(
|
||||
source: Pick<TaskDefinitionSource, 'listTaskDefinitions'>,
|
||||
projectId: string,
|
||||
input: ToolJsonValue,
|
||||
): Promise<Readonly<Record<string, ToolJsonValue>>> {
|
||||
const record =
|
||||
input && typeof input === 'object' && !Array.isArray(input)
|
||||
? (input as Readonly<Record<string, ToolJsonValue>>)
|
||||
: null;
|
||||
if (!record) return invalid('execution context or input is invalid');
|
||||
try {
|
||||
const result = await executeBoundedTaskListProjection(
|
||||
source,
|
||||
projectId,
|
||||
record,
|
||||
);
|
||||
return Object.freeze({
|
||||
tasks: Object.freeze(
|
||||
result.tasks.map((task) => Object.freeze({ ...task })),
|
||||
),
|
||||
hasMore: result.hasMore,
|
||||
...(result.next === undefined
|
||||
? {}
|
||||
: { next: Object.freeze({ ...result.next }) }),
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidBoundedTaskListProjectionError) {
|
||||
return invalid('execution context or input is invalid');
|
||||
}
|
||||
if (error instanceof BoundedTaskListProjectionUnavailableError) {
|
||||
throw new BuiltInTaskListToolUnavailableError();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
import type {
|
||||
TriggerCursor,
|
||||
TriggerRecord,
|
||||
TriggerSource,
|
||||
} from '@qinglong/runtime-core/trigger';
|
||||
import {
|
||||
normalizeToolDefinition,
|
||||
type ToolJsonValue,
|
||||
} from '@qinglong/runtime-core/tool-registry';
|
||||
|
||||
export const BUILTIN_TRIGGER_LIST_TOOL = Object.freeze({
|
||||
name: 'qinglong.trigger.list',
|
||||
version: '1.0.0',
|
||||
});
|
||||
export const BUILTIN_TRIGGER_LIST_TIMEOUT_SECONDS = 5;
|
||||
export const BUILTIN_TRIGGER_LIST_DEFAULT_LIMIT = 32;
|
||||
export const BUILTIN_TRIGGER_LIST_MAX_LIMIT = 64;
|
||||
|
||||
const MAX_INT = 2_147_483_647;
|
||||
const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
||||
const TRIGGER_SPEC_SCHEMA_PATTERN =
|
||||
/^[a-z][a-z0-9.-]{0,63}\/[a-z][a-z0-9.-]{0,63}@v[1-9][0-9]{0,5}$/;
|
||||
|
||||
const TRIGGER_LIST_CURSOR_SCHEMA = Object.freeze({
|
||||
type: 'object',
|
||||
properties: {
|
||||
triggerId: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
},
|
||||
required: ['triggerId'],
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
export const BUILTIN_TRIGGER_LIST_TOOL_DEFINITION = normalizeToolDefinition({
|
||||
name: BUILTIN_TRIGGER_LIST_TOOL.name,
|
||||
version: BUILTIN_TRIGGER_LIST_TOOL.version,
|
||||
description:
|
||||
'List current low-sensitive Triggers in the authenticated Project',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
after: TRIGGER_LIST_CURSOR_SCHEMA,
|
||||
limit: {
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
maximum: BUILTIN_TRIGGER_LIST_MAX_LIMIT,
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
additionalProperties: false,
|
||||
},
|
||||
outputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
triggers: {
|
||||
type: 'array',
|
||||
maxItems: BUILTIN_TRIGGER_LIST_MAX_LIMIT,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
triggerId: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
revision: { type: 'integer', minimum: 1, maximum: MAX_INT },
|
||||
taskId: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
taskRevision: {
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
maximum: MAX_INT,
|
||||
},
|
||||
specSchema: { type: 'string', minLength: 1, maxLength: 137 },
|
||||
enabled: { type: 'boolean' },
|
||||
updatedAtMs: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'triggerId',
|
||||
'revision',
|
||||
'taskId',
|
||||
'taskRevision',
|
||||
'specSchema',
|
||||
'enabled',
|
||||
'updatedAtMs',
|
||||
],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
hasMore: { type: 'boolean' },
|
||||
next: TRIGGER_LIST_CURSOR_SCHEMA,
|
||||
},
|
||||
required: ['triggers', 'hasMore'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
effect: 'read',
|
||||
risk: 'low',
|
||||
requiredPermissions: ['trigger.read'],
|
||||
timeoutSeconds: BUILTIN_TRIGGER_LIST_TIMEOUT_SECONDS,
|
||||
});
|
||||
|
||||
export class InvalidBuiltInTriggerListToolError extends TypeError {
|
||||
readonly code = 'BUILTIN_TRIGGER_LIST_TOOL_INVALID';
|
||||
|
||||
constructor(message: string) {
|
||||
super(`Built-in Trigger list Tool is invalid: ${message}`);
|
||||
this.name = 'InvalidBuiltInTriggerListToolError';
|
||||
}
|
||||
}
|
||||
|
||||
export class BuiltInTriggerListToolUnavailableError extends Error {
|
||||
readonly code = 'BUILTIN_TRIGGER_LIST_TOOL_UNAVAILABLE';
|
||||
|
||||
constructor() {
|
||||
super('Built-in Trigger list Tool is unavailable');
|
||||
this.name = 'BuiltInTriggerListToolUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
function invalid(message: string): never {
|
||||
throw new InvalidBuiltInTriggerListToolError(message);
|
||||
}
|
||||
|
||||
function boundedText(value: unknown, maximumBytes: number): value is string {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
value.length > 0 &&
|
||||
Buffer.byteLength(value, 'utf8') <= maximumBytes &&
|
||||
!CONTROL_PATTERN.test(value)
|
||||
);
|
||||
}
|
||||
|
||||
function integer(
|
||||
value: unknown,
|
||||
minimum: number,
|
||||
maximum: number,
|
||||
): value is number {
|
||||
return (
|
||||
Number.isSafeInteger(value) &&
|
||||
Number(value) >= minimum &&
|
||||
Number(value) <= maximum
|
||||
);
|
||||
}
|
||||
|
||||
function cursor(value: ToolJsonValue | undefined): TriggerCursor | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return invalid('cursor is invalid');
|
||||
}
|
||||
const record = value as Readonly<Record<string, ToolJsonValue>>;
|
||||
if (
|
||||
Reflect.ownKeys(record).length !== 1 ||
|
||||
!Object.hasOwn(record, 'triggerId') ||
|
||||
!boundedText(record.triggerId, 128)
|
||||
) {
|
||||
return invalid('cursor is invalid');
|
||||
}
|
||||
return Object.freeze({ triggerId: record.triggerId });
|
||||
}
|
||||
|
||||
function projectTrigger(
|
||||
value: TriggerRecord,
|
||||
projectId: string,
|
||||
after?: string,
|
||||
): Readonly<Record<string, ToolJsonValue>> | null {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
value.projectId !== projectId ||
|
||||
(after !== undefined && value.triggerId <= after) ||
|
||||
!boundedText(value.triggerId, 128) ||
|
||||
!integer(value.revision, 1, MAX_INT) ||
|
||||
!boundedText(value.taskId, 128) ||
|
||||
!integer(value.taskRevision, 1, MAX_INT) ||
|
||||
!boundedText(value.spec?.schema, 137) ||
|
||||
!TRIGGER_SPEC_SCHEMA_PATTERN.test(value.spec.schema) ||
|
||||
typeof value.enabled !== 'boolean' ||
|
||||
!integer(value.updatedAtMs, 0, Number.MAX_SAFE_INTEGER)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return Object.freeze({
|
||||
triggerId: value.triggerId,
|
||||
revision: value.revision,
|
||||
taskId: value.taskId,
|
||||
taskRevision: value.taskRevision,
|
||||
specSchema: value.spec.schema,
|
||||
enabled: value.enabled,
|
||||
updatedAtMs: value.updatedAtMs,
|
||||
});
|
||||
}
|
||||
|
||||
export async function executeBuiltInTriggerListTool(
|
||||
source: Pick<TriggerSource, 'listTriggers'>,
|
||||
projectId: string,
|
||||
input: ToolJsonValue,
|
||||
): Promise<Readonly<Record<string, ToolJsonValue>>> {
|
||||
const record =
|
||||
input && typeof input === 'object' && !Array.isArray(input)
|
||||
? (input as Readonly<Record<string, ToolJsonValue>>)
|
||||
: null;
|
||||
const keys = record ? Reflect.ownKeys(record) : [];
|
||||
if (
|
||||
typeof source?.listTriggers !== 'function' ||
|
||||
!boundedText(projectId, 128) ||
|
||||
!record ||
|
||||
keys.length > 2 ||
|
||||
keys.some((key) => key !== 'after' && key !== 'limit') ||
|
||||
(record.limit !== undefined &&
|
||||
!integer(record.limit, 1, BUILTIN_TRIGGER_LIST_MAX_LIMIT))
|
||||
) {
|
||||
return invalid('execution context or input is invalid');
|
||||
}
|
||||
const after = cursor(record.after);
|
||||
const limit = record.limit ?? BUILTIN_TRIGGER_LIST_DEFAULT_LIMIT;
|
||||
let page;
|
||||
try {
|
||||
page = await source.listTriggers({
|
||||
projectId,
|
||||
limit,
|
||||
...(after ? { after } : {}),
|
||||
});
|
||||
} catch {
|
||||
throw new BuiltInTriggerListToolUnavailableError();
|
||||
}
|
||||
if (
|
||||
!page ||
|
||||
!Array.isArray(page.triggers) ||
|
||||
page.triggers.length > limit ||
|
||||
typeof page.truncated !== 'boolean' ||
|
||||
page.truncated !== Boolean(page.next)
|
||||
) {
|
||||
throw new BuiltInTriggerListToolUnavailableError();
|
||||
}
|
||||
const triggers: Readonly<Record<string, ToolJsonValue>>[] = [];
|
||||
let boundary = after?.triggerId;
|
||||
for (const trigger of page.triggers) {
|
||||
const projected = projectTrigger(trigger, projectId, boundary);
|
||||
if (!projected) throw new BuiltInTriggerListToolUnavailableError();
|
||||
triggers.push(projected);
|
||||
boundary = trigger.triggerId;
|
||||
}
|
||||
if (
|
||||
page.truncated &&
|
||||
(!page.next ||
|
||||
Reflect.ownKeys(page.next).length !== 1 ||
|
||||
!boundedText(page.next.triggerId, 128) ||
|
||||
page.next.triggerId !== boundary ||
|
||||
triggers.length === 0)
|
||||
) {
|
||||
throw new BuiltInTriggerListToolUnavailableError();
|
||||
}
|
||||
return Object.freeze({
|
||||
triggers: Object.freeze(triggers),
|
||||
hasMore: page.truncated,
|
||||
...(page.truncated ? { next: Object.freeze({ triggerId: boundary! }) } : {}),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user