import { hostApiFetch } from '@/lib/host-api'; import type { DataServiceCollectionRemoval, DataServiceHostResult, DataServiceInstanceRemoval, DataServiceInstanceState, } from '../../shared/data-service'; import type { PluginBillingMode } from '../../shared/coding-plugins'; export type CodingPluginEffectiveState = | 'unavailable' | 'disabled' | 'identity_required' | 'authentication_required' | 'configuration_required' | 'ready' | 'degraded'; export type PluginPolicyStatus = 'current' | 'stale' | 'unavailable'; export type PluginBackend = | { status: 'not_required' | 'identity_required' | 'authentication_required' | 'unconfigured' | 'ready' } | { status: 'degraded'; code: string; message: string; retryable: boolean; retry_after_seconds?: number }; export type PluginBilling = { mode: PluginBillingMode; availability: 'available' | 'unavailable'; notice: string; pricingVersion?: number; unitName?: string; unitSize?: number; ratePoints?: string; minimumChargePoints?: string; roundingMode?: 'ceil'; }; export type CodingPluginItem = { id: string; version: string; contractVersion: number; requiresBackend: boolean; displayName: string; description: string; enabled: boolean; state: CodingPluginEffectiveState; backend: PluginBackend; skills: Array<{ id: string; assignedAgentIds: string[] }>; capabilities: Array<{ id: string; operations: Array<{ id: string; billing: PluginBilling; tool: { name: string; label: string; description: string; mutation: 'read' | 'write' | 'destructive'; permissions: string[]; } | null; }> }>; settingsSurface: string | null; }; export type CodingPluginProject = { schemaVersion: 1; project: { localProjectId: string; durableProjectId: string | null }; policyStatus: PluginPolicyStatus; unknownPluginIds: string[]; items: CodingPluginItem[]; }; export type CodingPluginFetcher = (path: string, init?: RequestInit) => Promise; function record(value: unknown, field: string): Record { if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${field} must be an object`); return value as Record; } function exact(value: Record, keys: readonly string[], field: string): void { const actual = Object.keys(value).sort(); const expected = [...keys].sort(); if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) { throw new Error(`${field} has unexpected fields`); } } function string(value: unknown, field: string, max = 256): string { if (typeof value !== 'string' || !value || value.length > max) throw new Error(`${field} is invalid`); return value; } function optionalNumber(value: unknown, field: string): number | undefined { if (value === undefined) return undefined; if (!Number.isFinite(value) || (value as number) < 0) throw new Error(`${field} is invalid`); return value as number; } function backend(value: unknown): PluginBackend { const source = record(value, 'backend'); const status = string(source.status, 'backend.status', 32) as PluginBackend['status']; if (['not_required', 'identity_required', 'authentication_required', 'unconfigured', 'ready'].includes(status)) { exact(source, ['status'], 'backend'); return { status } as PluginBackend; } if (status !== 'degraded') throw new Error('backend.status is invalid'); const allowed = ['status', 'code', 'message', 'retryable', 'retry_after_seconds']; if (Object.keys(source).some((key) => !allowed.includes(key))) throw new Error('backend has unexpected fields'); if (typeof source.retryable !== 'boolean') throw new Error('backend.retryable is invalid'); const retryAfter = optionalNumber(source.retry_after_seconds, 'backend.retry_after_seconds'); return { status, code: string(source.code, 'backend.code', 64), message: string(source.message, 'backend.message', 160), retryable: source.retryable, ...(retryAfter === undefined ? {} : { retry_after_seconds: retryAfter }), }; } function billing(value: unknown): PluginBilling { const source = record(value, 'billing'); const allowed = ['mode', 'availability', 'notice', 'pricingVersion', 'unitName', 'unitSize', 'ratePoints', 'minimumChargePoints', 'roundingMode']; if (Object.keys(source).some((key) => !allowed.includes(key))) throw new Error('billing has unexpected fields'); const mode = string(source.mode, 'billing.mode', 32) as PluginBillingMode; if (!['included', 'platform_metered', 'external_account'].includes(mode)) throw new Error('billing.mode is invalid'); const availability = string(source.availability, 'billing.availability', 16); if (availability !== 'available' && availability !== 'unavailable') throw new Error('billing.availability is invalid'); const result: PluginBilling = { mode, availability, notice: string(source.notice, 'billing.notice', 160) }; for (const key of ['pricingVersion', 'unitSize'] as const) { const parsed = optionalNumber(source[key], `billing.${key}`); if (parsed !== undefined) result[key] = parsed; } for (const key of ['unitName', 'ratePoints', 'minimumChargePoints'] as const) { if (source[key] !== undefined) result[key] = string(source[key], `billing.${key}`, 64); } if (source.roundingMode !== undefined) { if (source.roundingMode !== 'ceil') throw new Error('billing.roundingMode is invalid'); result.roundingMode = 'ceil'; } return result; } function pluginItem(value: unknown): CodingPluginItem { const source = record(value, 'plugin'); exact(source, ['id', 'version', 'contractVersion', 'requiresBackend', 'displayName', 'description', 'enabled', 'state', 'backend', 'skills', 'capabilities', 'settingsSurface'], 'plugin'); if (typeof source.enabled !== 'boolean' || typeof source.requiresBackend !== 'boolean' || !Number.isSafeInteger(source.contractVersion) || (source.contractVersion as number) < 1) { throw new Error('plugin metadata is invalid'); } const state = string(source.state, 'plugin.state', 32) as CodingPluginEffectiveState; if (!['unavailable', 'disabled', 'identity_required', 'authentication_required', 'configuration_required', 'ready', 'degraded'].includes(state)) throw new Error('plugin.state is invalid'); if (!Array.isArray(source.skills) || !Array.isArray(source.capabilities)) throw new Error('plugin arrays are invalid'); return { id: string(source.id, 'plugin.id', 128), version: string(source.version, 'plugin.version', 64), contractVersion: source.contractVersion as number, requiresBackend: source.requiresBackend, displayName: string(source.displayName, 'plugin.displayName'), description: string(source.description, 'plugin.description', 512), enabled: source.enabled, state, backend: backend(source.backend), skills: source.skills.map((value) => { const skill = record(value, 'skill'); exact(skill, ['id', 'assignedAgentIds'], 'skill'); if (!Array.isArray(skill.assignedAgentIds)) throw new Error('skill.assignedAgentIds is invalid'); return { id: string(skill.id, 'skill.id', 128), assignedAgentIds: skill.assignedAgentIds.map((id) => string(id, 'agent id', 128)) }; }), capabilities: source.capabilities.map((value) => { const capability = record(value, 'capability'); exact(capability, ['id', 'operations'], 'capability'); if (!Array.isArray(capability.operations)) throw new Error('capability.operations is invalid'); return { id: string(capability.id, 'capability.id', 128), operations: capability.operations.map((value) => { const operation = record(value, 'operation'); exact(operation, ['id', 'billing', 'tool'], 'operation'); let tool: CodingPluginItem['capabilities'][number]['operations'][number]['tool'] = null; if (operation.tool !== null) { const sourceTool = record(operation.tool, 'operation.tool'); exact(sourceTool, ['name', 'label', 'description', 'mutation', 'permissions'], 'operation.tool'); const mutation = string(sourceTool.mutation, 'operation.tool.mutation', 16); if (!['read', 'write', 'destructive'].includes(mutation) || !Array.isArray(sourceTool.permissions)) { throw new Error('operation.tool is invalid'); } tool = { name: string(sourceTool.name, 'operation.tool.name', 64), label: string(sourceTool.label, 'operation.tool.label', 128), description: string(sourceTool.description, 'operation.tool.description', 512), mutation: mutation as 'read' | 'write' | 'destructive', permissions: sourceTool.permissions.map((permission) => string(permission, 'operation.tool.permission', 64)), }; } return { id: string(operation.id, 'operation.id', 128), billing: billing(operation.billing), tool }; }) }; }), settingsSurface: source.settingsSurface === null ? null : string(source.settingsSurface, 'plugin.settingsSurface', 64), }; } export function parseCodingPluginProject(value: unknown): CodingPluginProject { const source = record(value, 'plugin project'); exact(source, ['schemaVersion', 'project', 'policyStatus', 'unknownPluginIds', 'items'], 'plugin project'); if (source.schemaVersion !== 1 || !Array.isArray(source.items) || !Array.isArray(source.unknownPluginIds)) throw new Error('plugin project schema is invalid'); const project = record(source.project, 'project'); exact(project, ['localProjectId', 'durableProjectId'], 'project'); const policyStatus = string(source.policyStatus, 'policyStatus', 16) as PluginPolicyStatus; if (!['current', 'stale', 'unavailable'].includes(policyStatus)) throw new Error('policyStatus is invalid'); return { schemaVersion: 1, project: { localProjectId: string(project.localProjectId, 'project.localProjectId', 128), durableProjectId: project.durableProjectId === null ? null : string(project.durableProjectId, 'project.durableProjectId', 128) }, policyStatus, unknownPluginIds: source.unknownPluginIds.map((id) => string(id, 'unknown plugin id', 128)), items: source.items.map(pluginItem), }; } const defaultFetch: CodingPluginFetcher = hostApiFetch; export async function getCodingPlugins(projectId: string, fetcher = defaultFetch): Promise { return parseCodingPluginProject(await fetcher(`/api/coding/plugins?projectId=${encodeURIComponent(projectId)}`)); } export async function setCodingPluginEnabled(projectId: string, pluginId: string, enabled: boolean, fetcher = defaultFetch): Promise { return parseCodingPluginProject(await fetcher(`/api/coding/plugins/${encodeURIComponent(pluginId)}`, { method: 'PUT', body: JSON.stringify({ projectId, enabled }), })); } function dataServiceResult(value: unknown): DataServiceHostResult { const source = record(value, 'Data Service result'); const allowed = ['success', 'status', 'code', 'error', 'retryable', 'retry_after_seconds', 'context', 'data']; if (Object.keys(source).some((key) => !allowed.includes(key)) || typeof source.success !== 'boolean' || !Number.isSafeInteger(source.status) || typeof source.retryable !== 'boolean') throw new Error('Data Service result is invalid'); return value as DataServiceHostResult; } export async function inspectDataService(fetcher = defaultFetch) { return dataServiceResult(await fetcher('/api/works/data-service/project')); } export async function configureDataService(collections: string[], fetcher = defaultFetch) { return dataServiceResult(await fetcher('/api/works/data-service/project', { method: 'PUT', body: JSON.stringify({ collections }) })); } export async function resetDataService(fetcher = defaultFetch) { return dataServiceResult(await fetcher('/api/works/data-service/project/reset?confirmed=true', { method: 'POST' })); } export async function removeDataServiceCollection(collection: string, fetcher = defaultFetch) { return dataServiceResult(await fetcher(`/api/works/data-service/project/collections/${encodeURIComponent(collection)}?confirmed=true`, { method: 'DELETE' })); } export async function removeDataServiceProject(fetcher = defaultFetch) { return dataServiceResult(await fetcher('/api/works/data-service/project?confirmed=true', { method: 'DELETE' })); }