import type { CodingProjectFileContent, CodingProjectFileEntry, CodingTextSearchResult, ConversationChangesSnapshot, ProductCodingCommand, ProductCodingSkill, ProductPiCommandInput, } from '../../shared/coding-product-tools'; import type { CodingAttachmentStore } from '../coding-projects/attachment-store'; import { readCodingProjectConfigV2 } from '../coding-projects/project-config'; import { CodingProjectFileService } from '../coding-projects/project-files'; import { CodingProjectServiceError, type CodingProjectService, } from '../coding-projects/project-service'; import type { CodingConversationService } from '../coding-runtime/conversation-service'; import type { CodingConversationRuntime } from '../coding-runtime/contracts'; import type { PiProductTools } from '../coding-runtime/pi/product-tools'; import type { DataServiceOperations } from '../services/data-service-client'; import type { PreviewDataSessionManager } from '../services/preview-data-session'; import { type CodingPluginDefinition, type PluginBillingMode, } from '../../shared/coding-plugins'; import type { CodingPluginAdapter, PluginBackendProjection, } from '../coding-plugins/registry'; import type { ProjectPluginService } from '../coding-plugins/project-service'; import type { PluginBillingPolicy, PluginCatalogOperation, PluginPolicyAvailability, PluginPolicyClient, PluginPolicyClientState, } from '../services/plugin-policy-client'; export interface ActiveCodingProject { id: string; path: string; } export interface CodingProductHost { fileStatus(): Promise; findFiles(query: string, limit?: number): Promise; fileContent(path: string): Promise; searchText(pattern: string): Promise; listSkills(agentId?: string): Promise; listCommands(conversationId: string): Promise; getChanges(conversationId: string): Promise; } export interface CodingProductComposition { attachments: CodingAttachmentStore; dataService: DataServiceOperations; previewDataSession?: PreviewDataSessionManager; productTools: PiProductTools; plugins: CodingProjectPluginService; projects: CodingProjectService; conversations: CodingConversationService; runtime: CodingConversationRuntime; host: CodingProductHost; sleep(reason: 'background_sleep' | 'auth_cleanup'): Promise; shutdown(): Promise; } export type CodingPluginEffectiveState = | 'unavailable' | 'disabled' | 'identity_required' | 'authentication_required' | 'configuration_required' | 'ready' | 'degraded'; export type PublicPluginBilling = Readonly<{ mode: PluginBillingMode; availability: 'available' | 'unavailable'; notice: string; pricingVersion?: number; unitName?: string; unitSize?: number; ratePoints?: string; minimumChargePoints?: string; roundingMode?: 'ceil'; }>; export interface CodingPluginProjectProjection { schemaVersion: 1; project: { localProjectId: string; durableProjectId: string | null; }; policyStatus: PluginPolicyAvailability; items: Array<{ id: string; version: string; contractVersion: number; requiresBackend: boolean; displayName: string; description: string; enabled: boolean; state: CodingPluginEffectiveState; backend: PluginBackendProjection; skills: Array<{ id: string; assignedAgentIds: string[] }>; capabilities: Array<{ id: string; operations: Array<{ id: string; billing: PublicPluginBilling; tool: { name: string; label: string; description: string; mutation: 'read' | 'write' | 'destructive'; permissions: string[]; } | null; }>; }>; settingsSurface: string | null; }>; } export interface CodingProjectPluginService { list(localProjectId: string): Promise; setEnabled( localProjectId: string, pluginId: string, enabled: boolean, ): Promise; deactivate(projectPath: string, pluginId?: string): Promise; } export class CodingProjectPluginServiceError extends Error { constructor( readonly status: 400 | 404 | 500 | 503, readonly code: string, message: string, ) { super(message); this.name = 'CodingProjectPluginServiceError'; } } export interface CreateCodingProjectPluginServiceOptions { projects: Pick; projectPlugins: Pick; policyClient: Pick; adapters: readonly CodingPluginAdapter[]; definitions: readonly CodingPluginDefinition[]; } function policyOperation( state: PluginPolicyClientState, definition: CodingPluginDefinition, capabilityId: string, operation: string, ): PluginCatalogOperation | null { const plugin = state.catalog?.plugins.find(({ plugin_id }) => plugin_id === definition.id); if (!plugin || !plugin.supported_contract_versions.includes(definition.contractVersion)) return null; return plugin.capabilities .find(({ capability_id }) => capability_id === capabilityId) ?.operations.find((candidate) => candidate.operation === operation) ?? null; } function publicBilling( policy: PluginBillingPolicy, state: PluginPolicyClientState, ): PublicPluginBilling { if (policy.mode === 'included' || policy.mode === 'external_account') { return { mode: policy.mode, availability: 'available', notice: policy.notice }; } if ('status' in policy && policy.status === 'billing_unavailable') { return { mode: 'platform_metered', availability: 'unavailable', notice: policy.notice, }; } const pricingVersion = state.catalog?.pricing_version?.version; return { mode: 'platform_metered', availability: 'available', notice: policy.notice, ...(pricingVersion === undefined ? {} : { pricingVersion }), unitName: policy.unit_name, unitSize: policy.unit_size, ratePoints: policy.rate_points, minimumChargePoints: policy.minimum_charge_points, roundingMode: policy.rounding_mode, }; } function degradedBackend(): PluginBackendProjection { return { status: 'degraded', code: 'plugin_backend_unavailable', message: 'Plugin backend is temporarily unavailable', retryable: true, }; } function boundedBackend(value: PluginBackendProjection): PluginBackendProjection { switch (value.status) { case 'not_required': return { status: 'not_required' }; case 'identity_required': return { status: 'identity_required' }; case 'authentication_required': return { status: 'authentication_required' }; case 'unconfigured': return { status: 'unconfigured' }; case 'ready': return { status: 'ready' }; case 'degraded': { const code = typeof value.code === 'string' && /^[a-z][a-z0-9_]{0,63}$/u.test(value.code) ? value.code : 'plugin_backend_unavailable'; const message = typeof value.message === 'string' && value.message.length > 0 && value.message.length <= 160 ? value.message : 'Plugin backend is temporarily unavailable'; const retryAfter = value.retry_after_seconds; return { status: 'degraded', code, message, retryable: value.retryable === true, ...(Number.isSafeInteger(retryAfter) && (retryAfter as number) >= 0 && (retryAfter as number) <= 86_400 ? { retry_after_seconds: retryAfter } : {}), }; } } } function effectiveState(input: { enabled: boolean; policyAvailable: boolean; backend: PluginBackendProjection; }): CodingPluginEffectiveState { if (!input.policyAvailable) return 'unavailable'; if (!input.enabled) return 'disabled'; switch (input.backend.status) { case 'identity_required': return 'identity_required'; case 'authentication_required': return 'authentication_required'; case 'unconfigured': return 'configuration_required'; case 'ready': case 'not_required': return 'ready'; case 'degraded': return 'degraded'; } } /** * Bounded Renderer-facing projection. It resolves a local project handle in * Main, joins only code-owned package fields with verified policy, and keeps * adapters and project paths behind the composition boundary. */ export function createCodingProjectPluginService( options: CreateCodingProjectPluginServiceOptions, ): CodingProjectPluginService { const definitions = options.definitions; const adapters = new Map(options.adapters.map((adapter) => [adapter.pluginId, adapter])); async function project(localProjectId: string) { const id = localProjectId.trim(); if (!id) { throw new CodingProjectPluginServiceError(400, 'plugin_request_invalid', 'projectId is required'); } return await options.projects.getProject(id); } async function list(localProjectId: string): Promise { const localProject = await project(localProjectId); await options.policyClient.refresh(); const policy = options.policyClient.getState(); const [selection, config] = await Promise.all([ options.projectPlugins.getEnabledPluginIds(localProject.path), readCodingProjectConfigV2(localProject.path), ]); const durableProjectId = config.status === 'valid' ? config.config.projectId ?? null : null; const items = await Promise.all(definitions.map(async (definition) => { const enabled = selection.includes(definition.id); const adapter = adapters.get(definition.id); let backend: PluginBackendProjection; try { backend = adapter ? boundedBackend(await adapter.inspect(localProject.path)) : degradedBackend(); } catch { backend = degradedBackend(); } const pluginPolicy = policy.catalog?.plugins.find(({ plugin_id }) => plugin_id === definition.id); const policyAvailable = Boolean( pluginPolicy?.supported_contract_versions.includes(definition.contractVersion), ); const capabilityIds = [...new Set(definition.operations.map(({ capabilityId }) => capabilityId))]; const capabilities = capabilityIds.flatMap((capabilityId) => { const operations = definition.operations .filter((candidate) => candidate.capabilityId === capabilityId) .flatMap((candidate) => { const joined = policyOperation(policy, definition, capabilityId, candidate.operation); if (!joined) return []; const tool = candidate.toolName ? definition.tools.find(({ name }) => name === candidate.toolName) : undefined; return [{ id: candidate.operation, billing: publicBilling(joined.billing, policy), tool: tool ? { name: tool.name, label: tool.label, description: tool.description, mutation: tool.mutation, permissions: [...tool.permissions], } : null, }]; }); return operations.length > 0 ? [{ id: capabilityId, operations }] : []; }); const agents = config.status === 'valid' ? config.config.agents : []; return { id: definition.id, version: definition.version, contractVersion: definition.contractVersion, requiresBackend: definition.requiresBackend, displayName: definition.displayName, description: definition.description, enabled, state: effectiveState({ enabled, policyAvailable, backend }), backend, skills: definition.skills.map((skill) => ({ id: skill.id, assignedAgentIds: agents .filter((agent) => agent.skillIds.includes(skill.id)) .map(({ id }) => id) .sort(), })), capabilities, settingsSurface: definition.surfaces.projectSettings ?? null, }; })); return { schemaVersion: 1, project: { localProjectId: localProject.id, durableProjectId }, policyStatus: policy.status, items, }; } return { list, async setEnabled(localProjectId, pluginId, enabled) { if (typeof enabled !== 'boolean') { throw new CodingProjectPluginServiceError(400, 'plugin_request_invalid', 'enabled must be boolean'); } const localProject = await project(localProjectId); await options.projectPlugins.setEnabled(localProject.path, pluginId, enabled); return await list(localProject.id); }, async deactivate(projectPath, pluginId) { const targets = pluginId ? [adapters.get(pluginId)].filter(Boolean) : [...adapters.values()]; await Promise.allSettled(targets.map(async (adapter) => { await adapter.deactivate?.(projectPath); })); }, }; } export class CodingProductHostError extends Error { constructor( readonly status: 404 | 409, readonly code: string, message: string, ) { super(message); } } export interface CodingProductHostOptions { projects: Pick; productTools: PiProductTools; files?: CodingProjectFileService; getEnabledPluginIds?(projectPath: string): Promise; listPiCommands?(conversationId: string): Promise; } function normalizePiCommands(value: unknown): ProductPiCommandInput[] { const record = value && typeof value === 'object' && !Array.isArray(value) ? value as Record : null; const candidates = Array.isArray(value) ? value : Array.isArray(record?.commands) ? record.commands : Array.isArray(record?.data) ? record.data : []; return candidates.flatMap((candidate) => { if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) return []; const command = candidate as Record; if (typeof command.name !== 'string') return []; return [{ name: command.name, ...(typeof command.description === 'string' ? { description: command.description } : {}), }]; }); } export function createCodingProductHost(options: CodingProductHostOptions): CodingProductHost { const files = options.files ?? new CodingProjectFileService(); async function activeProject(): Promise { const project = await options.projects.getActiveProject(); if (!project) { throw new CodingProductHostError( 409, 'CODING_ACTIVE_PROJECT_REQUIRED', 'No active coding project is selected', ); } return project; } async function selectedSkillIds( projectPath: string, agentId?: string, ): Promise { if (!agentId) return []; const config = await readCodingProjectConfigV2(projectPath); if (config.status !== 'valid') { throw new CodingProductHostError( 409, 'CODING_PROJECT_CONFIG_INVALID', 'Coding project configuration is unavailable', ); } const agent = config.config.agents.find((candidate) => ( candidate.id === agentId && candidate.enabled && !candidate.archivedAt )); if (!agent) { throw new CodingProductHostError( 404, 'CODING_AGENT_NOT_FOUND', 'Coding project Agent does not exist', ); } return agent.skillIds; } async function conversationContext(conversationId: string): Promise<{ project: ActiveCodingProject; skillIds: readonly string[]; }> { let context; try { context = await options.projects.findActiveConversation(conversationId); } catch (error) { if (error instanceof CodingProjectServiceError && (error.status === 404 || error.status === 409)) { throw new CodingProductHostError(error.status, error.code, error.message); } throw error; } const { project, conversation } = context; return { project, skillIds: await selectedSkillIds(project.path, conversation.agentId), }; } return { async fileStatus() { const project = await activeProject(); return await files.status(project.path); }, async findFiles(query, limit) { const project = await activeProject(); return await files.find(project.path, query, limit); }, async fileContent(filePath) { const project = await activeProject(); return await files.content(project.path, filePath); }, async searchText(pattern) { const project = await activeProject(); return await files.search(project.path, pattern); }, async listSkills(agentId) { const project = await activeProject(); const enabledPluginIds = options.getEnabledPluginIds ? await options.getEnabledPluginIds(project.path) : []; return await options.productTools.listSkills( await selectedSkillIds(project.path, agentId), enabledPluginIds, ); }, async listCommands(conversationId) { const context = await conversationContext(conversationId); const enabledPluginIds = options.getEnabledPluginIds ? await options.getEnabledPluginIds(context.project.path) : []; const piCommands = options.listPiCommands ? normalizePiCommands(await options.listPiCommands(conversationId)) : []; return await options.productTools.listCommands(context.skillIds, piCommands, enabledPluginIds); }, async getChanges(conversationId) { await conversationContext(conversationId); return options.productTools.getChanges(conversationId); }, }; }