717 lines
30 KiB
TypeScript
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.resolve({
|
|
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);
|