Files
makelore/electron/coding-plugins/registry.ts

717 lines
30 KiB
TypeScript

import {
type CodingPluginDefinition,
type CodingPluginToolDefinition,
type PluginBillingMode,
} from '../../shared/coding-plugins';
import { CORE_CODING_SKILL_IDS } from '../../shared/coding-skills';
import type {
CapabilityBillingReceiptV1,
CapabilityResultV1,
} from '../../shared/data-service';
import type { PiProductToolContext, PiProductToolResult } from '../coding-runtime/pi/product-tools';
import type {
PluginCatalog,
PluginCatalogOperation,
PluginPolicyClientState,
} from '../services/plugin-policy-client';
import type {
EffectivePluginResolver,
EffectivePluginSnapshot,
EffectivePluginSkillSource,
} from './effective-resolver';
const MAX_REQUEST_ID = 128;
const PLUGIN_ID_PATTERN = /^[a-z][a-z0-9.-]{0,47}$/u;
const CAPABILITY_ID_PATTERN = /^[a-z][a-z0-9.-]{0,63}$/u;
const OPERATION_PATTERN = /^[a-z][a-z0-9._-]{0,63}$/u;
const VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9.+-]{0,63}$/u;
const DECIMAL_PATTERN = /^(?:0|[1-9]\d*)(?:\.\d+)?$/u;
const CONTEXT_KEYS = new Set([
'resource', 'limit', 'current', 'attempted', 'actual', 'current_revision',
]);
const CONTEXT_RESOURCES = new Set(['instances', 'collections', 'documents', 'bytes']);
export interface TrustedCodingCapabilityContext {
conversationId: string;
runId: string;
resourceId: string;
requestId: string;
localProjectId: string;
projectPath: string;
durableProjectId: string;
workerRole: 'parent' | 'child';
effectiveSkillIds: readonly string[];
pluginReleaseId?: string;
}
export type PluginBackendProjection =
| { status: 'not_required' }
| { status: 'identity_required' }
| { status: 'authentication_required' }
| { status: 'unconfigured' }
| { status: 'ready' }
| {
status: 'degraded';
code: string;
message: string;
retryable: boolean;
retry_after_seconds?: number;
};
export type AdapterInvocationResult<T = unknown> =
| {
success: true;
status: number;
code: null;
error: null;
retryable: false;
payload_schema: string;
data: T | null;
billing?: CapabilityBillingReceiptV1;
}
| {
success: false;
status: number;
code: string;
error: string;
retryable: boolean;
retry_after_seconds?: number;
context?: Readonly<Record<string, string | number>>;
payload_schema: string;
data: null;
billing?: CapabilityBillingReceiptV1;
};
export interface CodingPluginAdapter {
readonly pluginId: string;
inspect(projectPath: string): Promise<PluginBackendProjection>;
invoke(
context: TrustedCodingCapabilityContext,
tool: CodingPluginToolDefinition,
input: unknown,
): Promise<AdapterInvocationResult>;
deactivate?(projectPath: string): Promise<void>;
}
export interface ResolvedWorkerResources {
catalogRevision: number;
pluginIds: readonly string[];
effectiveSkillIds: readonly string[];
skillEntries: readonly { id: string; entryPath: string; packageRoot?: string }[];
tools: readonly CodingPluginToolDefinition[];
/** The exact Main-owned snapshot used to produce these legacy fields. */
effectiveSnapshot?: EffectivePluginSnapshot;
/** Trusted package roots paired with the same effective snapshot. */
skillRoots?: readonly string[];
}
export interface CodingCapabilityRegistryPort {
resolveWorkerResources(input: {
projectPath: string;
assignedSkillIds: readonly string[];
role: 'parent' | 'child';
projectId?: string;
}): Promise<ResolvedWorkerResources>;
resolveEffectivePluginSnapshot?(input: {
projectId: string;
projectPath: string;
assignedSkillIds: readonly string[];
role: 'parent' | 'child';
}): Promise<EffectivePluginSnapshot>;
invoke(input: {
toolName: string;
context: PiProductToolContext;
workerRole: 'parent' | 'child';
effectiveSkillIds: readonly string[];
value: unknown;
}): Promise<PiProductToolResult>;
}
export type CodingCapabilityRegistry = CodingCapabilityRegistryPort;
export interface CodingCapabilityRegistryOptions {
policyClient: {
getState(): PluginPolicyClientState;
refresh(): Promise<void>;
};
getEnabledPluginIds?: (projectPath: string) => Promise<readonly string[]>;
projectPlugins?: {
getEnabledPluginIds(projectPath: string): Promise<readonly string[]>;
};
adapters: readonly CodingPluginAdapter[];
definitions: readonly CodingPluginDefinition[];
getDurableProjectId?: (projectPath: string, localProjectId: string) => Promise<string> | string;
effectiveResolver?: EffectivePluginResolver;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function boundedText(value: unknown, maximum: number, fallback: string): string {
return typeof value === 'string' && value.trim().length > 0 && value.length <= maximum
? value
: fallback;
}
function validRequestPart(value: unknown): value is string {
return typeof value === 'string' && value.length > 0 && value.length <= MAX_REQUEST_ID;
}
function requestId(context: PiProductToolContext): string {
const runId = context.runId;
const resourceId = context.resourceId;
const result = `pi:${runId}:${resourceId}`;
return validRequestPart(runId) && validRequestPart(resourceId) && result.length <= MAX_REQUEST_ID
? result
: 'invalid-request-id';
}
function boundedStatus(value: unknown, fallback: number): number {
return Number.isSafeInteger(value) && (value as number) >= 100 && (value as number) <= 599
? value as number
: fallback;
}
function boundedRetry(value: unknown): number | undefined {
return Number.isSafeInteger(value) && (value as number) >= 0 && (value as number) <= 86_400
? value as number
: undefined;
}
function boundedContext(value: unknown): Readonly<Record<string, string | number>> | undefined {
if (!isRecord(value)) return undefined;
const result: Record<string, string | number> = {};
for (const [key, item] of Object.entries(value)) {
if (!CONTEXT_KEYS.has(key)) continue;
if (key === 'resource') {
if (typeof item !== 'string' || item.length === 0 || item.length > 32
|| !CONTEXT_RESOURCES.has(item)) continue;
result[key] = item;
} else if (Number.isSafeInteger(item) && (item as number) >= 0) {
result[key] = item as number;
}
}
return Object.keys(result).length > 0 ? result : undefined;
}
function validDecimal(value: unknown): value is string {
return typeof value === 'string' && value.length > 0 && value.length <= 32
&& DECIMAL_PATTERN.test(value);
}
function validBillingReceipt(value: unknown): value is CapabilityBillingReceiptV1 {
if (!isRecord(value) || typeof value.mode !== 'string' || typeof value.status !== 'string') return false;
const keys = new Set(Object.keys(value));
if (value.status === 'not_started') {
return keys.size === 2 && ['unknown', 'included', 'platform_metered', 'external_account'].includes(value.mode);
}
if (value.mode === 'included' && value.status === 'included') return keys.size === 2;
if (value.mode === 'external_account' && value.status === 'external') return keys.size === 2;
if (value.mode === 'platform_metered' && value.status === 'receipt_unavailable') {
return keys.size === 2;
}
if (value.mode !== 'platform_metered' || !validDecimal(value.reserved_points)) return false;
const common = new Set(['mode', 'status', 'reserved_points', 'actual_points', 'usage_amount', 'unit']);
if (keys.size !== [...keys].filter((key) => common.has(key)).length) return false;
if (value.actual_points !== undefined && !validDecimal(value.actual_points)) return false;
if (value.usage_amount !== undefined
&& (typeof value.usage_amount !== 'number' || !Number.isFinite(value.usage_amount)
|| value.usage_amount < 0 || value.usage_amount > Number.MAX_SAFE_INTEGER)) return false;
if (value.unit !== undefined
&& (typeof value.unit !== 'string' || value.unit.length === 0 || value.unit.length > 80)) return false;
const status = value.status;
if (['settled', 'refunded'].includes(status)) return value.actual_points !== undefined;
return ['reserved', 'dispatched', 'released', 'pending_review', 'expired'].includes(status);
}
function schemaValid(value: unknown, schema: Readonly<Record<string, unknown>>): boolean {
if (schema.type === 'object') {
if (!isRecord(value)) return false;
const properties = isRecord(schema.properties) ? schema.properties : {};
if (schema.additionalProperties === false
&& Object.keys(value).some((key) => !Object.prototype.hasOwnProperty.call(properties, key))) return false;
const required = Array.isArray(schema.required) ? schema.required : [];
if (required.some((key) => typeof key !== 'string' || !Object.prototype.hasOwnProperty.call(value, key))) return false;
return Object.entries(value).every(([key, item]) => {
const child = properties[key];
return !child || (isRecord(child) && schemaValid(item, child));
});
}
if (schema.type === 'array') {
if (!Array.isArray(value)) return false;
if (typeof schema.maxItems === 'number' && value.length > schema.maxItems) return false;
const items = isRecord(schema.items) ? schema.items : undefined;
return !items || value.every((item) => schemaValid(item, items));
}
if (schema.type === 'string') {
if (typeof value !== 'string') return false;
if (typeof schema.minLength === 'number' && value.length < schema.minLength) return false;
if (typeof schema.maxLength === 'number' && value.length > schema.maxLength) return false;
if (typeof schema.pattern === 'string' && !(new RegExp(schema.pattern, 'u')).test(value)) return false;
return true;
}
if (schema.type === 'integer') {
if (!Number.isSafeInteger(value)) return false;
if (typeof schema.minimum === 'number' && (value as number) < schema.minimum) return false;
if (typeof schema.maximum === 'number' && (value as number) > schema.maximum) return false;
return true;
}
if (schema.type === 'boolean') {
return typeof value === 'boolean' && (schema.const === undefined || value === schema.const);
}
return true;
}
function policyOperation(
catalog: PluginCatalog,
definition: CodingPluginDefinition,
tool: CodingPluginToolDefinition,
): PluginCatalogOperation | null {
const plugin = catalog.plugins.find(({ plugin_id }) => plugin_id === definition.id);
if (!plugin || plugin.status !== 'active' || !plugin.supported_contract_versions.includes(definition.contractVersion)) return null;
const capability = plugin.capabilities.find(({ capability_id }) => capability_id === tool.capabilityId);
return capability?.operations.find(({ operation }) => operation === tool.operation) ?? null;
}
function definitionValid(definition: CodingPluginDefinition): boolean {
if (!PLUGIN_ID_PATTERN.test(definition.id) || !VERSION_PATTERN.test(definition.version)
|| !Number.isSafeInteger(definition.contractVersion) || definition.contractVersion < 1
|| definition.scope !== 'project' || typeof definition.requiresBackend !== 'boolean') return false;
if (!definition.skills.length
|| (definition.runtimeKind !== 'skill_only' && !definition.tools.length)
|| (definition.runtimeKind === 'skill_only' && (definition.requiresBackend || definition.tools.length > 0))) return false;
const skillIds = new Set<string>();
for (const skill of definition.skills) {
if (!skill.id || skillIds.has(skill.id) || !skill.entryPath
|| (definition.runtimeKind !== 'skill_only' && skill.grants.length === 0)
|| (definition.runtimeKind === 'skill_only' && skill.grants.length > 0)) return false;
skillIds.add(skill.id);
}
const toolNames = new Set<string>();
for (const tool of definition.tools) {
if (!tool.name || toolNames.has(tool.name) || tool.roles.length !== 1 || tool.roles[0] !== 'parent'
|| !CAPABILITY_ID_PATTERN.test(tool.capabilityId) || !OPERATION_PATTERN.test(tool.operation)
|| !isRecord(tool.inputSchema) || tool.inputSchema.type !== 'object'
|| tool.inputSchema.additionalProperties !== false) return false;
toolNames.add(tool.name);
}
return true;
}
function policyNotStarted(mode: PluginBillingMode | 'unknown'): CapabilityBillingReceiptV1 {
return { mode, status: 'not_started' };
}
function policyEntered(mode: PluginBillingMode): CapabilityBillingReceiptV1 {
if (mode === 'included') return { mode, status: 'included' };
if (mode === 'external_account') return { mode, status: 'external' };
return { mode, status: 'not_started' };
}
function resultDetails(
definition: CodingPluginDefinition,
tool: CodingPluginToolDefinition,
context: PiProductToolContext,
result: AdapterInvocationResult,
billing: CapabilityBillingReceiptV1,
): CapabilityResultV1 {
const id = requestId(context);
const rawStatus = boundedStatus(result.status, 502);
const success = result.success && rawStatus >= 200 && rawStatus <= 299;
const status = success ? rawStatus : rawStatus >= 400 ? rawStatus : 502;
const payloadSchema = boundedText(result.payload_schema, 128, 'unknown');
const code = success ? null : result.success
? 'plugin_backend_unavailable'
: boundedText(result.code, 128, 'plugin_backend_unavailable');
const details: CapabilityResultV1 = {
schema: 'makelore-capability.v1',
plugin_id: definition.id,
plugin_version: definition.version,
capability_id: tool.capabilityId,
operation: tool.operation,
request_id: id,
success,
status,
code,
error: success ? null : boundedText(result.success ? null : result.error, 2_000, 'Plugin capability failed'),
retryable: success ? false : result.success ? true : result.retryable,
...(success || result.success ? {} : (boundedRetry(result.retry_after_seconds) === undefined
? {} : { retry_after_seconds: boundedRetry(result.retry_after_seconds) })),
...(success || result.success ? {} : (boundedContext(result.context) === undefined
? {} : { context: boundedContext(result.context) })),
billing,
payload_schema: payloadSchema,
data: success ? (result.data ?? null) : null,
};
return details;
}
export function buildCapabilityToolResult(
definition: CodingPluginDefinition,
tool: CodingPluginToolDefinition,
context: PiProductToolContext,
result: AdapterInvocationResult,
billing: CapabilityBillingReceiptV1 = { mode: 'included', status: 'included' },
): PiProductToolResult {
const details = resultDetails(definition, tool, context, result, billing);
return {
content: [{ type: 'text', text: JSON.stringify(details) }],
details,
};
}
export class CodingCapabilityRegistryImpl implements CodingCapabilityRegistryPort {
private readonly definitions: readonly CodingPluginDefinition[];
private readonly toolsByName: ReadonlyMap<string, { definition: CodingPluginDefinition; tool: CodingPluginToolDefinition }>;
private readonly adaptersByPluginId: ReadonlyMap<string, CodingPluginAdapter>;
constructor(private readonly options: CodingCapabilityRegistryOptions) {
const definitions = options.definitions.filter(definitionValid);
this.definitions = Object.freeze([...definitions]);
const tools = new Map<string, { definition: CodingPluginDefinition; tool: CodingPluginToolDefinition }>();
for (const definition of this.definitions) {
for (const tool of definition.tools) {
if (!tools.has(tool.name)) tools.set(tool.name, { definition, tool });
}
}
this.toolsByName = tools;
this.adaptersByPluginId = new Map(options.adapters.map((adapter) => [adapter.pluginId, adapter]));
}
async resolveWorkerResources(input: {
projectPath: string;
assignedSkillIds: readonly string[];
role: 'parent' | 'child';
projectId?: string;
}): Promise<ResolvedWorkerResources> {
if (this.options.effectiveResolver) {
const snapshot = await this.options.effectiveResolver.resolve({
projectId: input.projectId ?? input.projectPath,
projectPath: input.projectPath,
assignedSkillIds: input.assignedSkillIds,
role: input.role,
});
const sources = await this.options.effectiveResolver.getSkillSources();
const effectiveSkills = new Set(snapshot.effectiveSkillIds);
const pluginIds = new Set<string>(snapshot.runtimePolicies.map(({ pluginId }) => pluginId));
const skillRoots = new Set<string>();
const sourcesBySkillId = new Map<string, EffectivePluginSkillSource>();
for (const source of sources) {
if (effectiveSkills.has(source.id) && !sourcesBySkillId.has(source.id)) {
sourcesBySkillId.set(source.id, source);
}
}
const skillEntries = snapshot.skillEntries.map(({ id, entryPath, packageRoot }) => {
const source = sourcesBySkillId.get(id);
const pairedRoot = packageRoot ?? source?.packageRoot;
if (source && pairedRoot) {
pluginIds.add(source.pluginId);
skillRoots.add(pairedRoot);
}
return {
id,
entryPath,
...(pairedRoot ? { packageRoot: pairedRoot } : {}),
};
});
return {
catalogRevision: this.options.effectiveResolver.getPolicyState().revision,
pluginIds: [...pluginIds],
effectiveSkillIds: [...snapshot.effectiveSkillIds],
skillEntries,
tools: snapshot.toolDefinitions.map((tool) => structuredClone(tool)),
effectiveSnapshot: snapshot,
skillRoots: [...skillRoots],
};
}
const assigned = [...new Set(input.assignedSkillIds)];
const coreIds = new Set<string>(CORE_CODING_SKILL_IDS);
const pluginSkillOwners = new Map<string, CodingPluginDefinition>();
for (const definition of this.definitions) {
for (const skill of definition.skills) pluginSkillOwners.set(skill.id, definition);
}
for (const id of assigned) {
if (!coreIds.has(id) && !pluginSkillOwners.has(id)) throw new Error(`Unknown bundled coding skill: ${id}`);
}
const effectiveCoreSkills = assigned.filter((id) => coreIds.has(id));
if (input.role === 'child') {
return {
catalogRevision: this.options.policyClient.getState().revision,
pluginIds: [],
effectiveSkillIds: effectiveCoreSkills,
skillEntries: effectiveCoreSkills.map((id) => ({ id, entryPath: `${id}/SKILL.md` })),
tools: [],
};
}
const hasAssignedServerPlugin = this.definitions.some((definition) => (
definition.requiresBackend
&& this.adaptersByPluginId.has(definition.id)
&& definition.skills.some(({ id }) => assigned.includes(id))
));
if (!hasAssignedServerPlugin) {
return {
catalogRevision: this.options.policyClient.getState().revision,
pluginIds: [],
effectiveSkillIds: effectiveCoreSkills,
skillEntries: effectiveCoreSkills.map((id) => ({ id, entryPath: `${id}/SKILL.md` })),
tools: [],
};
}
await this.options.policyClient.refresh();
const enabled = await this.enabledPluginIds(input.projectPath);
const state = this.options.policyClient.getState();
const pluginIds: string[] = [];
const effectiveSkillIds = [...effectiveCoreSkills];
const skillEntries = effectiveCoreSkills.map((id) => ({ id, entryPath: `${id}/SKILL.md` }));
const tools: CodingPluginToolDefinition[] = [];
for (const definition of this.definitions) {
if (!definition.requiresBackend || !enabled.includes(definition.id)
|| !this.adaptersByPluginId.has(definition.id)) continue;
const pluginPolicy = state.catalog ? state.catalog.plugins.find(({ plugin_id }) => plugin_id === definition.id) : undefined;
if (!pluginPolicy || pluginPolicy.status !== 'active' || !pluginPolicy.supported_contract_versions.includes(definition.contractVersion)) continue;
pluginIds.push(definition.id);
const selectedSkills = definition.skills.filter(({ id }) => assigned.includes(id));
if (selectedSkills.length === 0) continue;
for (const skill of selectedSkills) {
effectiveSkillIds.push(skill.id);
skillEntries.push({
id: skill.id,
entryPath: skill.entryPath,
...(definition.provenance.source === 'marketplace'
? { packageRoot: definition.provenance.packageRoot }
: {}),
});
}
for (const tool of definition.tools) {
const granted = selectedSkills.some(({ grants }) => grants.includes(tool.capabilityId));
const policy = state.catalog ? policyOperation(state.catalog, definition, tool) : null;
const available = policy !== null && (
policy.billing.mode !== 'platform_metered'
|| !('status' in policy.billing && policy.billing.status === 'billing_unavailable')
);
if (granted && available) {
tools.push(tool);
}
}
}
return {
catalogRevision: state.revision,
pluginIds,
effectiveSkillIds: [...new Set(effectiveSkillIds)],
skillEntries,
tools,
};
}
async resolveEffectivePluginSnapshot(input: {
projectId: string;
projectPath: string;
assignedSkillIds: readonly string[];
role: 'parent' | 'child';
}): Promise<EffectivePluginSnapshot> {
if (this.options.effectiveResolver) return await this.options.effectiveResolver.resolve(input);
const resources = await this.resolveWorkerResources(input);
return Object.freeze({
accountSessionId: 'anonymous',
projectId: input.projectId,
pluginReleaseIds: Object.freeze([]),
effectiveSkillIds: Object.freeze([...resources.effectiveSkillIds]),
skillEntries: Object.freeze(resources.skillEntries.map((entry) => Object.freeze({ ...entry }))),
toolDefinitions: Object.freeze(resources.tools.map((tool) => structuredClone(tool))),
runtimePolicies: Object.freeze([]),
unavailableReasons: Object.freeze([]),
});
}
async invoke(input: {
toolName: string;
context: PiProductToolContext;
workerRole: 'parent' | 'child';
effectiveSkillIds: readonly string[];
value: unknown;
}): Promise<PiProductToolResult> {
let indexed = this.toolsByName.get(input.toolName);
const validId = requestId(input.context) !== 'invalid-request-id';
if (!indexed && this.options.effectiveResolver && input.context.effectiveSnapshot) {
const snapshotTool = input.context.effectiveSnapshot.toolDefinitions
.find(({ name }) => name === input.toolName);
const policy = snapshotTool
? input.context.effectiveSnapshot.runtimePolicies.find((candidate) => (
candidate.capabilityId === snapshotTool.capabilityId
&& candidate.operation === snapshotTool.operation
))
: undefined;
const definition = policy
? await this.options.effectiveResolver.getInstalledDefinition(policy.pluginId, policy.releaseId)
: null;
const tool = definition?.tools.find((candidate) => (
candidate.name === input.toolName
&& candidate.capabilityId === snapshotTool?.capabilityId
&& candidate.operation === snapshotTool.operation
));
if (definition && tool) indexed = { definition, tool };
}
if (!indexed) return this.unknownResult(input.context, 'plugin_backend_unavailable', 'Plugin capability is unavailable');
const { definition, tool } = indexed;
if (this.options.effectiveResolver && input.context.effectiveSnapshot) {
const frozenPolicy = input.context.effectiveSnapshot.runtimePolicies.find((candidate) => (
candidate.pluginId === definition.id
&& candidate.capabilityId === tool.capabilityId
&& candidate.operation === tool.operation
));
const currentSnapshot = await this.options.effectiveResolver.resolveForInvocation({
projectId: input.context.projectId,
projectPath: input.context.projectPath,
assignedSkillIds: input.context.effectiveSnapshot.effectiveSkillIds,
role: input.workerRole,
});
const currentPolicy = currentSnapshot.runtimePolicies.find((candidate) => (
candidate.pluginId === definition.id
&& candidate.capabilityId === tool.capabilityId
&& candidate.operation === tool.operation
));
if (currentSnapshot.accountSessionId !== input.context.effectiveSnapshot.accountSessionId
|| !currentSnapshot.toolDefinitions.some(({ name }) => name === input.toolName)
|| frozenPolicy?.releaseId !== currentPolicy?.releaseId
|| frozenPolicy?.pluginVersion !== currentPolicy?.pluginVersion) {
const disabled = currentSnapshot.unavailableReasons.some((reason) => (
reason.pluginId === definition.id && reason.code === 'project_disabled'
));
return this.resultFailure(
definition,
tool,
input.context,
disabled ? 'plugin_not_enabled' : 'plugin_runtime_stale',
disabled ? 'Plugin is not enabled for this project' : 'Plugin worker resources are stale',
disabled ? 403 : 409,
false,
policyNotStarted('unknown'),
);
}
}
// The effective resolver may refresh a stale policy while checking the
// worker snapshot. Read the post-resolution state for invocation checks.
const state = this.options.policyClient.getState();
const currentEnabled = await this.enabledPluginIds(input.context.projectPath);
const catalogPolicy = state.catalog ? policyOperation(state.catalog, definition, tool) : null;
const baseBilling = catalogPolicy ? policyNotStarted(catalogPolicy.billing.mode) : policyNotStarted('unknown');
if (!validId) return this.resultFailure(definition, tool, input.context, 'plugin_input_invalid', 'Pi request identity is invalid', 422, false, baseBilling);
if (input.workerRole !== 'parent' || !tool.roles.includes(input.workerRole)) {
return this.resultFailure(definition, tool, input.context, 'plugin_permission_denied', 'Plugin capability is not permitted for this worker', 403, false, baseBilling);
}
if (state.status === 'unavailable' || !state.catalog) {
return this.unknownResult(input.context, 'plugin_backend_unavailable', 'Plugin policy is unavailable');
}
if (!currentEnabled.includes(definition.id)) {
return this.resultFailure(definition, tool, input.context, 'plugin_not_enabled', 'Plugin is not enabled for this project', 403, false, baseBilling);
}
if (!catalogPolicy) {
return this.resultFailure(definition, tool, input.context, 'plugin_contract_unsupported', 'Plugin capability is not supported by the server policy', 503, true, policyNotStarted('unknown'));
}
const selectedSkill = definition.skills.find(({ id }) => input.effectiveSkillIds.includes(id));
if (!selectedSkill || !selectedSkill.grants.includes(tool.capabilityId)) {
return this.resultFailure(definition, tool, input.context, 'plugin_permission_denied', 'Plugin Skill grant is required', 403, false, baseBilling);
}
if (catalogPolicy.billing.mode === 'platform_metered'
&& 'status' in catalogPolicy.billing && catalogPolicy.billing.status === 'billing_unavailable') {
return this.resultFailure(definition, tool, input.context, 'plugin_billing_unavailable', 'Plugin billing is unavailable', 503, true, baseBilling);
}
if (!schemaValid(input.value, tool.inputSchema)) {
return this.resultFailure(definition, tool, input.context, 'plugin_input_invalid', 'Plugin tool input is invalid', 422, false, baseBilling);
}
const adapter = this.adaptersByPluginId.get(definition.id);
if (!adapter) return this.resultFailure(definition, tool, input.context, 'plugin_backend_unavailable', 'Plugin adapter is unavailable', 503, true, baseBilling);
const durableProjectId = await this.options.getDurableProjectId?.(input.context.projectPath, input.context.projectId)
?? input.context.projectId;
const trustedContext: TrustedCodingCapabilityContext = {
conversationId: input.context.conversationId,
runId: input.context.runId,
resourceId: input.context.resourceId,
requestId: requestId(input.context),
localProjectId: input.context.projectId,
projectPath: input.context.projectPath,
durableProjectId,
workerRole: input.workerRole,
effectiveSkillIds: [...input.effectiveSkillIds],
...(definition.releaseId ? { pluginReleaseId: definition.releaseId } : {}),
};
let result: AdapterInvocationResult;
try {
result = await adapter.invoke(trustedContext, tool, input.value);
} catch {
return this.resultFailure(definition, tool, input.context, 'plugin_backend_unavailable', 'Plugin adapter is temporarily unavailable', 503, true, baseBilling);
}
let billing: CapabilityBillingReceiptV1;
if (catalogPolicy.billing.mode === 'platform_metered') {
if (!result.billing || !validBillingReceipt(result.billing) || result.billing.mode !== 'platform_metered') {
return this.resultFailure(definition, tool, input.context, 'plugin_billing_unavailable', 'Plugin billing receipt is unavailable', 503, true, policyNotStarted('platform_metered'));
}
billing = result.billing;
} else {
billing = policyEntered(catalogPolicy.billing.mode);
}
return buildCapabilityToolResult(definition, tool, input.context, result, billing);
}
private async enabledPluginIds(projectPath: string): Promise<readonly string[]> {
if (this.options.getEnabledPluginIds) return await this.options.getEnabledPluginIds(projectPath);
if (this.options.projectPlugins) return await this.options.projectPlugins.getEnabledPluginIds(projectPath);
return [];
}
private resultFailure(
definition: CodingPluginDefinition,
tool: CodingPluginToolDefinition,
context: PiProductToolContext,
code: string,
error: string,
status: number,
retryable: boolean,
billing: CapabilityBillingReceiptV1,
): PiProductToolResult {
return buildCapabilityToolResult(definition, tool, context, {
success: false,
status,
code,
error,
retryable,
payload_schema: 'unknown',
data: null,
}, billing);
}
private unknownResult(
context: PiProductToolContext,
code: string,
error: string,
): PiProductToolResult {
const details: CapabilityResultV1 = {
schema: 'makelore-capability.v1',
plugin_id: 'unknown',
plugin_version: 'unknown',
capability_id: 'unknown',
operation: 'unknown',
request_id: requestId(context),
success: false,
status: 503,
code,
error,
retryable: true,
billing: { mode: 'unknown', status: 'not_started' },
payload_schema: 'unknown',
data: null,
};
return {
content: [{ type: 'text', text: JSON.stringify(details) }],
details,
};
}
}
export const CodingCapabilityRegistry = CodingCapabilityRegistryImpl;
export const createCodingCapabilityRegistry = (
options: CodingCapabilityRegistryOptions,
): CodingCapabilityRegistry => new CodingCapabilityRegistryImpl(options);