318 lines
14 KiB
TypeScript
318 lines
14 KiB
TypeScript
import type {
|
|
AgentBrowserDetailsV1,
|
|
CapabilityBillingReceiptV1,
|
|
CapabilityResultV1,
|
|
ChangedFileDetailsV1,
|
|
GameAssetsDetailsV1,
|
|
KnownToolDetails,
|
|
RuntimeContextDetailsV1,
|
|
TaskStateDetailsV1,
|
|
} from './coding-conversation-contracts';
|
|
import { DATA_SERVICE_PI_TOOL_NAMES } from './data-service';
|
|
|
|
const PRODUCT_TOOL_NAMES = new Set([
|
|
'agent_browser',
|
|
'game_asset_browser',
|
|
'game_asset_review',
|
|
'task_state',
|
|
'changed_file',
|
|
'runtime_context',
|
|
...DATA_SERVICE_PI_TOOL_NAMES,
|
|
]);
|
|
|
|
const CAPABILITY_CONTEXT_KEYS = new Set([
|
|
'resource', 'limit', 'current', 'attempted', 'actual', 'current_revision',
|
|
]);
|
|
const CAPABILITY_CONTEXT_RESOURCES = new Set(['instances', 'collections', 'documents', 'bytes']);
|
|
const CAPABILITY_ID_PATTERN = /^[a-z][a-z0-9.-]{0,63}$/u;
|
|
const CAPABILITY_OPERATION_PATTERN = /^[a-z][a-z0-9._-]{0,63}$/u;
|
|
const CAPABILITY_PLUGIN_ID_PATTERN = /^[a-z][a-z0-9.-]{0,47}$/u;
|
|
const CAPABILITY_DECIMAL_PATTERN = /^(?:0|[1-9]\d*)(?:\.\d+)?$/u;
|
|
const MAX_CAPABILITY_DATA_BYTES = 1_310_720;
|
|
|
|
function record(value: unknown): Record<string, unknown> | null {
|
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
? value as Record<string, unknown>
|
|
: null;
|
|
}
|
|
|
|
function text(value: unknown, max = 4096): string | null {
|
|
return typeof value === 'string' && value.trim() && value.length <= max ? value : null;
|
|
}
|
|
|
|
function strings(value: unknown, maxItems = 200): string[] | null {
|
|
if (!Array.isArray(value) || value.length > maxItems) return null;
|
|
const result: string[] = [];
|
|
for (const item of value) {
|
|
const normalized = text(item);
|
|
if (!normalized) return null;
|
|
result.push(normalized);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function relativePaths(value: unknown): string[] | null {
|
|
const paths = strings(value);
|
|
if (!paths) return null;
|
|
const result: string[] = [];
|
|
for (const candidate of paths) {
|
|
const normalized = candidate.replaceAll('\\', '/').replace(/^\.\//, '');
|
|
if (!normalized || normalized.startsWith('/') || /^[A-Za-z]:\//.test(normalized)
|
|
|| normalized.split('/').some((segment) => segment === '..')) return null;
|
|
result.push(normalized);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function browserDetails(value: Record<string, unknown>): AgentBrowserDetailsV1 | null {
|
|
const actions = new Set([
|
|
'open', 'status', 'close', 'reset_profile', 'navigate', 'send_cdp', 'read_events', 'read_payload',
|
|
]);
|
|
if (!actions.has(String(value.action))) return null;
|
|
const attachmentId = value.attachmentId === undefined ? undefined : text(value.attachmentId, 128);
|
|
const mime = value.mime === undefined ? undefined : text(value.mime, 128);
|
|
if ((value.attachmentId !== undefined && !attachmentId) || (value.mime !== undefined && !mime)) return null;
|
|
if (Boolean(attachmentId) !== Boolean(mime) || (attachmentId && value.action !== 'send_cdp')) return null;
|
|
return {
|
|
schema: 'agent-browser.v1',
|
|
action: value.action as AgentBrowserDetailsV1['action'],
|
|
...(attachmentId ? { attachmentId } : {}),
|
|
...(mime ? { mime } : {}),
|
|
};
|
|
}
|
|
|
|
function gameAssetDetails(value: Record<string, unknown>): GameAssetsDetailsV1 | null {
|
|
const invocationId = text(value.invocationId, 200);
|
|
const candidateIds = strings(value.candidateIds);
|
|
const pendingAssetIds = strings(value.pendingAssetIds);
|
|
const approvedAssetIds = strings(value.approvedAssetIds);
|
|
const discardedAssetIds = strings(value.discardedAssetIds);
|
|
if (!invocationId || !candidateIds || !pendingAssetIds || !approvedAssetIds || !discardedAssetIds) return null;
|
|
if (value.status !== 'pending' && value.status !== 'resolved') return null;
|
|
return {
|
|
schema: 'game-assets.v1',
|
|
invocationId,
|
|
candidateIds,
|
|
status: value.status,
|
|
pendingAssetIds,
|
|
approvedAssetIds,
|
|
discardedAssetIds,
|
|
};
|
|
}
|
|
|
|
function taskDetails(value: Record<string, unknown>): TaskStateDetailsV1 | null {
|
|
if (!Array.isArray(value.tasks) || value.tasks.length === 0 || value.tasks.length > 100) return null;
|
|
const tasks: TaskStateDetailsV1['tasks'] = [];
|
|
for (const candidate of value.tasks) {
|
|
const item = record(candidate);
|
|
const id = text(item?.id, 128);
|
|
const title = text(item?.title, 500);
|
|
if (!item || !id || !title || !['pending', 'running', 'complete', 'error'].includes(String(item.status))) {
|
|
return null;
|
|
}
|
|
tasks.push({ id, title, status: item.status as TaskStateDetailsV1['tasks'][number]['status'] });
|
|
}
|
|
return { schema: 'task-state.v1', tasks };
|
|
}
|
|
|
|
function runtimeContextDetails(value: Record<string, unknown>): RuntimeContextDetailsV1 | null {
|
|
if (!Array.isArray(value.skills) || value.skills.length > 100
|
|
|| !Array.isArray(value.commands) || value.commands.length > 200) return null;
|
|
const skills: RuntimeContextDetailsV1['skills'] = [];
|
|
const commands: RuntimeContextDetailsV1['commands'] = [];
|
|
for (const candidate of value.skills) {
|
|
const item = record(candidate);
|
|
const id = text(item?.id, 128);
|
|
const name = text(item?.name, 200);
|
|
if (!item || !id || !name || typeof item.description !== 'string' || item.description.length > 2000
|
|
|| typeof item.selected !== 'boolean') return null;
|
|
skills.push({ id, name, description: item.description, selected: item.selected });
|
|
}
|
|
for (const candidate of value.commands) {
|
|
const item = record(candidate);
|
|
const name = text(item?.name, 128);
|
|
const title = text(item?.title, 200);
|
|
if (!item || !name || !title || typeof item.description !== 'string' || item.description.length > 2000
|
|
|| !['makelore', 'pi', 'skill'].includes(String(item.source))) return null;
|
|
const skillId = item.skillId === undefined ? undefined : text(item.skillId, 128);
|
|
if (item.skillId !== undefined && !skillId) return null;
|
|
commands.push({
|
|
name,
|
|
title,
|
|
description: item.description,
|
|
source: item.source as RuntimeContextDetailsV1['commands'][number]['source'],
|
|
...(skillId ? { skillId } : {}),
|
|
});
|
|
}
|
|
return { schema: 'runtime-context.v1', skills, commands };
|
|
}
|
|
|
|
function capabilityContext(value: unknown): Readonly<Record<string, string | number>> | undefined | null {
|
|
if (value === undefined) return undefined;
|
|
const source = record(value);
|
|
if (!source || Object.keys(source).length > 6) return null;
|
|
const context: Record<string, string | number> = {};
|
|
for (const [key, item] of Object.entries(source)) {
|
|
if (!CAPABILITY_CONTEXT_KEYS.has(key)) return null;
|
|
if (key === 'resource') {
|
|
if (typeof item !== 'string' || item.length === 0 || item.length > 32
|
|
|| !CAPABILITY_CONTEXT_RESOURCES.has(item)) return null;
|
|
context[key] = item;
|
|
} else if (Number.isSafeInteger(item) && (item as number) >= 0) {
|
|
context[key] = item as number;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
return Object.keys(context).length > 0 ? context : undefined;
|
|
}
|
|
|
|
function exactKeys(value: Record<string, unknown>, required: readonly string[], optional: readonly string[] = []): boolean {
|
|
const allowed = new Set([...required, ...optional]);
|
|
return required.every((key) => Object.prototype.hasOwnProperty.call(value, key))
|
|
&& Object.keys(value).every((key) => allowed.has(key));
|
|
}
|
|
|
|
function capabilityBilling(value: unknown): CapabilityBillingReceiptV1 | null {
|
|
const billing = record(value);
|
|
if (!billing || typeof billing.mode !== 'string' || typeof billing.status !== 'string') return null;
|
|
if (billing.status === 'not_started') {
|
|
if (!exactKeys(billing, ['mode', 'status'])
|
|
|| !['unknown', 'included', 'platform_metered', 'external_account'].includes(billing.mode)) return null;
|
|
return {
|
|
mode: billing.mode as CapabilityBillingReceiptV1['mode'],
|
|
status: 'not_started',
|
|
};
|
|
}
|
|
if (billing.mode === 'included' && billing.status === 'included') {
|
|
return exactKeys(billing, ['mode', 'status']) ? { mode: 'included', status: 'included' } : null;
|
|
}
|
|
if (billing.mode === 'external_account' && billing.status === 'external') {
|
|
return exactKeys(billing, ['mode', 'status'])
|
|
? { mode: 'external_account', status: 'external' }
|
|
: null;
|
|
}
|
|
if (billing.mode !== 'platform_metered' || typeof billing.reserved_points !== 'string'
|
|
|| !CAPABILITY_DECIMAL_PATTERN.test(billing.reserved_points)
|
|
|| billing.reserved_points.length > 32) return null;
|
|
if (!exactKeys(billing, ['mode', 'status', 'reserved_points'], ['actual_points', 'usage_amount', 'unit'])) return null;
|
|
if (billing.actual_points !== undefined
|
|
&& (typeof billing.actual_points !== 'string' || billing.actual_points.length > 32
|
|
|| !CAPABILITY_DECIMAL_PATTERN.test(billing.actual_points))) return null;
|
|
if (billing.usage_amount !== undefined
|
|
&& (typeof billing.usage_amount !== 'number' || !Number.isFinite(billing.usage_amount)
|
|
|| billing.usage_amount < 0 || billing.usage_amount > Number.MAX_SAFE_INTEGER)) return null;
|
|
if (billing.unit !== undefined
|
|
&& (typeof billing.unit !== 'string' || billing.unit.length === 0 || billing.unit.length > 80)) return null;
|
|
if (['settled', 'refunded'].includes(billing.status)) {
|
|
return billing.actual_points === undefined ? null : {
|
|
mode: 'platform_metered',
|
|
status: billing.status as 'settled' | 'refunded',
|
|
reserved_points: billing.reserved_points,
|
|
actual_points: billing.actual_points,
|
|
...(billing.usage_amount === undefined ? {} : { usage_amount: billing.usage_amount as number }),
|
|
...(billing.unit === undefined ? {} : { unit: billing.unit as string }),
|
|
};
|
|
}
|
|
if (!['reserved', 'dispatched', 'released', 'pending_review', 'expired'].includes(billing.status)) return null;
|
|
return {
|
|
mode: 'platform_metered',
|
|
status: billing.status as 'reserved' | 'dispatched' | 'released' | 'pending_review' | 'expired',
|
|
reserved_points: billing.reserved_points,
|
|
...(billing.actual_points === undefined ? {} : { actual_points: billing.actual_points as string }),
|
|
...(billing.usage_amount === undefined ? {} : { usage_amount: billing.usage_amount as number }),
|
|
...(billing.unit === undefined ? {} : { unit: billing.unit as string }),
|
|
};
|
|
}
|
|
|
|
function capabilityDetails(value: Record<string, unknown>): CapabilityResultV1 | null {
|
|
const required = [
|
|
'schema', 'plugin_id', 'plugin_version', 'capability_id', 'operation', 'request_id',
|
|
'success', 'status', 'code', 'error', 'retryable', 'billing', 'payload_schema', 'data',
|
|
];
|
|
if (!exactKeys(value, required, ['retry_after_seconds', 'context'])
|
|
|| value.schema !== 'makelore-capability.v1') return null;
|
|
if (typeof value.plugin_id !== 'string' || value.plugin_id.length > 48
|
|
|| !CAPABILITY_PLUGIN_ID_PATTERN.test(value.plugin_id)
|
|
|| !text(value.plugin_version, 64)
|
|
|| typeof value.capability_id !== 'string' || !CAPABILITY_ID_PATTERN.test(value.capability_id)
|
|
|| typeof value.operation !== 'string' || !CAPABILITY_OPERATION_PATTERN.test(value.operation)
|
|
|| typeof value.request_id !== 'string' || value.request_id.length === 0 || value.request_id.length > 128
|
|
|| typeof value.payload_schema !== 'string' || value.payload_schema.length === 0 || value.payload_schema.length > 128) return null;
|
|
if (typeof value.success !== 'boolean'
|
|
|| !Number.isSafeInteger(value.status)
|
|
|| (value.status as number) < 100 || (value.status as number) > 599
|
|
|| (value.code !== null && text(value.code, 128) === null)
|
|
|| (value.error !== null && text(value.error, 2_000) === null)
|
|
|| typeof value.retryable !== 'boolean'
|
|
|| !Object.prototype.hasOwnProperty.call(value, 'data')) return null;
|
|
if (value.success) {
|
|
if ((value.status as number) < 200 || (value.status as number) > 299
|
|
|| value.code !== null || value.error !== null || value.retryable !== false) return null;
|
|
} else if ((value.status as number) < 400 || value.code === null || value.error === null) {
|
|
return null;
|
|
}
|
|
const retryAfter = value.retry_after_seconds;
|
|
if (retryAfter !== undefined
|
|
&& (!Number.isSafeInteger(retryAfter) || (retryAfter as number) < 0 || (retryAfter as number) > 86_400)) {
|
|
return null;
|
|
}
|
|
const context = capabilityContext(value.context);
|
|
if (context === null) return null;
|
|
const billing = capabilityBilling(value.billing);
|
|
if (!billing) return null;
|
|
const data = value.data === null ? null : record(value.data);
|
|
if (value.data !== null && !data) return null;
|
|
if (data !== null) {
|
|
try {
|
|
const encoded = JSON.stringify(data);
|
|
if (typeof encoded !== 'string' || new TextEncoder().encode(encoded).byteLength > MAX_CAPABILITY_DATA_BYTES) {
|
|
return null;
|
|
}
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
return {
|
|
schema: 'makelore-capability.v1',
|
|
plugin_id: value.plugin_id as string,
|
|
plugin_version: value.plugin_version as string,
|
|
capability_id: value.capability_id as string,
|
|
operation: value.operation as string,
|
|
request_id: value.request_id as string,
|
|
success: value.success,
|
|
status: value.status as number,
|
|
code: value.code as string | null,
|
|
error: value.error as string | null,
|
|
retryable: value.retryable,
|
|
...(retryAfter === undefined ? {} : { retry_after_seconds: retryAfter as number }),
|
|
...(context === undefined ? {} : { context }),
|
|
billing,
|
|
payload_schema: value.payload_schema as string,
|
|
data,
|
|
};
|
|
}
|
|
|
|
export function productToolDetails(value: unknown): Exclude<KnownToolDetails, { schema: 'subagent.v1' | 'write-lease.v1' }> | null {
|
|
const details = record(value);
|
|
if (!details) return null;
|
|
if (details.schema === 'changed-file.v1') {
|
|
const paths = relativePaths(details.paths);
|
|
return paths ? { schema: 'changed-file.v1', paths } satisfies ChangedFileDetailsV1 : null;
|
|
}
|
|
if (details.schema === 'task-state.v1') return taskDetails(details);
|
|
if (details.schema === 'agent-browser.v1') return browserDetails(details);
|
|
if (details.schema === 'game-assets.v1') return gameAssetDetails(details);
|
|
if (details.schema === 'runtime-context.v1') return runtimeContextDetails(details);
|
|
if (details.schema === 'makelore-capability.v1') return capabilityDetails(details);
|
|
return null;
|
|
}
|
|
|
|
export function productToolDetailsOfResult(value: unknown) {
|
|
return productToolDetails(record(value)?.details);
|
|
}
|
|
|
|
export function isProductToolName(value: string): boolean {
|
|
return PRODUCT_TOOL_NAMES.has(value);
|
|
}
|