feat(coding): add plugin capability policy registry
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import type { DataServiceToolDetailsV1 } from './data-service';
|
||||
import type { CapabilityResultV1 } from './data-service';
|
||||
|
||||
export type { CapabilityBillingReceiptV1, CapabilityResultV1 } from './data-service';
|
||||
|
||||
export type ConversationThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high';
|
||||
|
||||
@@ -194,8 +196,6 @@ export interface RuntimeContextDetailsV1 {
|
||||
}>;
|
||||
}
|
||||
|
||||
export type { DataServiceToolDetailsV1 } from './data-service';
|
||||
|
||||
export interface SubagentDetailsV1 {
|
||||
schema: 'subagent.v1';
|
||||
dispatchId: string;
|
||||
@@ -218,7 +218,7 @@ export type KnownToolDetails =
|
||||
| AgentBrowserDetailsV1
|
||||
| GameAssetsDetailsV1
|
||||
| RuntimeContextDetailsV1
|
||||
| DataServiceToolDetailsV1
|
||||
| CapabilityResultV1
|
||||
| SubagentDetailsV1;
|
||||
|
||||
export interface ConversationToolNode {
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import type {
|
||||
AgentBrowserDetailsV1,
|
||||
CapabilityBillingReceiptV1,
|
||||
CapabilityResultV1,
|
||||
ChangedFileDetailsV1,
|
||||
DataServiceToolDetailsV1,
|
||||
GameAssetsDetailsV1,
|
||||
KnownToolDetails,
|
||||
RuntimeContextDetailsV1,
|
||||
TaskStateDetailsV1,
|
||||
} from './coding-conversation-contracts';
|
||||
import {
|
||||
DATA_SERVICE_PI_TOOL_NAMES,
|
||||
type DataServiceErrorContext,
|
||||
type DataServiceToolData,
|
||||
type DataServicePiToolName,
|
||||
} from './data-service';
|
||||
import { DATA_SERVICE_PI_TOOL_NAMES } from './data-service';
|
||||
|
||||
const PRODUCT_TOOL_NAMES = new Set([
|
||||
'agent_browser',
|
||||
@@ -24,17 +20,15 @@ const PRODUCT_TOOL_NAMES = new Set([
|
||||
...DATA_SERVICE_PI_TOOL_NAMES,
|
||||
]);
|
||||
|
||||
const DATA_SERVICE_CONTEXT_KEYS = new Set([
|
||||
'resource',
|
||||
'limit',
|
||||
'current',
|
||||
'attempted',
|
||||
'actual',
|
||||
'allowed',
|
||||
'current_revision',
|
||||
'retry_after_seconds',
|
||||
const CAPABILITY_CONTEXT_KEYS = new Set([
|
||||
'resource', 'limit', 'current', 'attempted', 'actual', 'current_revision',
|
||||
]);
|
||||
const DATA_SERVICE_CONTEXT_RESOURCES = new Set(['instances', 'collections', 'documents', 'bytes']);
|
||||
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)
|
||||
@@ -153,15 +147,16 @@ function runtimeContextDetails(value: Record<string, unknown>): RuntimeContextDe
|
||||
return { schema: 'runtime-context.v1', skills, commands };
|
||||
}
|
||||
|
||||
function dataServiceContext(value: unknown): DataServiceErrorContext | undefined | null {
|
||||
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 > 8) return null;
|
||||
const context: DataServiceErrorContext = {};
|
||||
if (!source || Object.keys(source).length > 6) return null;
|
||||
const context: Record<string, string | number> = {};
|
||||
for (const [key, item] of Object.entries(source)) {
|
||||
if (!DATA_SERVICE_CONTEXT_KEYS.has(key)) return null;
|
||||
if (!CAPABILITY_CONTEXT_KEYS.has(key)) return null;
|
||||
if (key === 'resource') {
|
||||
if (typeof item !== 'string' || !DATA_SERVICE_CONTEXT_RESOURCES.has(item)) return null;
|
||||
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;
|
||||
@@ -172,8 +167,78 @@ function dataServiceContext(value: unknown): DataServiceErrorContext | undefined
|
||||
return Object.keys(context).length > 0 ? context : undefined;
|
||||
}
|
||||
|
||||
function dataServiceDetails(value: Record<string, unknown>): DataServiceToolDetailsV1 | null {
|
||||
if (!DATA_SERVICE_PI_TOOL_NAMES.includes(value.operation as DataServicePiToolName)) return null;
|
||||
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
|
||||
@@ -192,13 +257,29 @@ function dataServiceDetails(value: Record<string, unknown>): DataServiceToolDeta
|
||||
&& (!Number.isSafeInteger(retryAfter) || (retryAfter as number) < 0 || (retryAfter as number) > 86_400)) {
|
||||
return null;
|
||||
}
|
||||
const context = dataServiceContext(value.context);
|
||||
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: 'data-service.v1',
|
||||
operation: value.operation as DataServicePiToolName,
|
||||
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,
|
||||
@@ -206,7 +287,9 @@ function dataServiceDetails(value: Record<string, unknown>): DataServiceToolDeta
|
||||
retryable: value.retryable,
|
||||
...(retryAfter === undefined ? {} : { retry_after_seconds: retryAfter as number }),
|
||||
...(context === undefined ? {} : { context }),
|
||||
data: data as DataServiceToolData | null,
|
||||
billing,
|
||||
payload_schema: value.payload_schema as string,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -221,7 +304,7 @@ export function productToolDetails(value: unknown): Exclude<KnownToolDetails, {
|
||||
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 === 'data-service.v1') return dataServiceDetails(details);
|
||||
if (details.schema === 'makelore-capability.v1') return capabilityDetails(details);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,65 @@ export type DataServiceDocumentList = {
|
||||
|
||||
export type DataServiceErrorContext = Record<string, string | number>;
|
||||
|
||||
export type PluginBillingMode =
|
||||
| 'included'
|
||||
| 'platform_metered'
|
||||
| 'external_account';
|
||||
|
||||
/**
|
||||
* A closed billing projection. Token Point amounts stay decimal strings all
|
||||
* the way through the client boundary; no UI or adapter is allowed to invent
|
||||
* an amount or a terminal receipt.
|
||||
*/
|
||||
export type CapabilityBillingReceiptV1 =
|
||||
| {
|
||||
mode: 'unknown' | PluginBillingMode;
|
||||
status: 'not_started';
|
||||
}
|
||||
| {
|
||||
mode: 'included';
|
||||
status: 'included';
|
||||
}
|
||||
| {
|
||||
mode: 'external_account';
|
||||
status: 'external';
|
||||
}
|
||||
| {
|
||||
mode: 'platform_metered';
|
||||
status: 'reserved' | 'dispatched' | 'released' | 'pending_review' | 'expired';
|
||||
reserved_points: string;
|
||||
actual_points?: string;
|
||||
usage_amount?: number;
|
||||
unit?: string;
|
||||
}
|
||||
| {
|
||||
mode: 'platform_metered';
|
||||
status: 'settled' | 'refunded';
|
||||
reserved_points: string;
|
||||
actual_points: string;
|
||||
usage_amount?: number;
|
||||
unit?: string;
|
||||
};
|
||||
|
||||
export interface CapabilityResultV1<T = unknown> {
|
||||
schema: 'makelore-capability.v1';
|
||||
plugin_id: string;
|
||||
plugin_version: string;
|
||||
capability_id: string;
|
||||
operation: string;
|
||||
request_id: string;
|
||||
success: boolean;
|
||||
status: number;
|
||||
code: string | null;
|
||||
error: string | null;
|
||||
retryable: boolean;
|
||||
retry_after_seconds?: number;
|
||||
context?: Readonly<Record<string, string | number>>;
|
||||
billing: CapabilityBillingReceiptV1;
|
||||
payload_schema: string;
|
||||
data: T | null;
|
||||
}
|
||||
|
||||
export const DATA_SERVICE_PI_TOOL_NAMES = [
|
||||
'data_service_configure',
|
||||
'data_service_inspect',
|
||||
@@ -96,9 +155,9 @@ export type DataServiceToolData =
|
||||
| DataServiceCollectionRemoval
|
||||
| DataServiceInstanceRemoval;
|
||||
|
||||
export type DataServiceToolDetailsV1 = DataServiceHostResult<DataServiceToolData> & {
|
||||
schema: 'data-service.v1';
|
||||
operation: DataServicePiToolName;
|
||||
/** @deprecated Use the registry-owned capability envelope with this payload schema. */
|
||||
export type DataServiceToolDetailsV1 = CapabilityResultV1<DataServiceToolData> & {
|
||||
payload_schema: 'data-service.v1';
|
||||
};
|
||||
|
||||
export type DataServicePutDocumentInput = {
|
||||
|
||||
Reference in New Issue
Block a user