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'; import type { CatalogPage, CatalogQuery, MarketplaceClient, PluginDetail, } from '../coding-plugins/marketplace-client'; import type { InstallationSnapshot, PluginPackageStore, } from '../coding-plugins/package-store'; import type { MarketplaceLibrarySnapshot } from '../coding-plugins/account-plugin-cache'; import type { EffectivePluginResolver } from '../coding-plugins/effective-resolver'; import type { DevicePackageManager } from '../coding-packages/device-package-manager'; 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; devicePackages: DevicePackageManager; previewDataSession?: PreviewDataSessionManager; productTools: PiProductTools; pluginMarketplace: CodingPluginMarketplaceService; /** Alias retained for Main callers that refer to the Marketplace service directly. */ marketplace: CodingPluginMarketplaceService; plugins: CodingProjectPluginService; projects: CodingProjectService; conversations: CodingConversationService; runtime: CodingConversationRuntime; host: CodingProductHost; sleep(reason: 'background_sleep' | 'auth_cleanup'): Promise; shutdown(): Promise; } export type PublicPluginInstallation = Readonly<{ status: InstallationSnapshot['status']; pluginId: string; releaseId?: string; version?: string; channel?: 'stable' | 'beta'; reason?: string; }>; export type PublicPluginLibrary = Readonly<{ library: MarketplaceLibrarySnapshot; installations: readonly PublicPluginInstallation[]; }>; /** Main-owned Marketplace facade. It never exposes package roots or manifests. */ export interface CodingPluginMarketplaceService { readCatalog(input?: CatalogQuery): Promise; readDetail(pluginId: string): Promise; readLibrary(): Promise; acquire(pluginId: string): Promise; remove(pluginId: string): Promise; install(pluginId: string): Promise; installBeta(pluginId: string): Promise; update(pluginId: string): Promise; uninstall(pluginId: string): Promise; } export interface CreateCodingPluginMarketplaceServiceOptions { marketplace: MarketplaceClient; packageStore: PluginPackageStore; clientVersion: string; onChanged?(event: { pluginId: string; kind: 'acquire' | 'remove' | 'install' | 'update' | 'uninstall'; }): Promise | void; } function publicInstallation(snapshot: InstallationSnapshot): PublicPluginInstallation { return { status: snapshot.status, pluginId: snapshot.pluginId, ...(snapshot.releaseId ? { releaseId: snapshot.releaseId } : {}), ...(snapshot.version ? { version: snapshot.version } : {}), ...(snapshot.channel ? { channel: snapshot.channel } : {}), ...(snapshot.reason ? { reason: snapshot.reason } : {}), }; } async function publicLibrary( library: MarketplaceLibrarySnapshot, packageStore: Pick, ): Promise { const installations = await Promise.all(library.items.map(async ({ pluginId }) => { try { const record = await packageStore.getInstalled(pluginId); return record ? { status: record.unavailableReason ? 'unavailable' as const : 'installed' as const, pluginId: record.pluginId, releaseId: record.releaseId, version: record.version, ...(record.channel === undefined ? {} : { channel: record.channel }), ...(record.unavailableReason ? { reason: record.unavailableReason } : {}), } : null; } catch (error) { const candidate = error && typeof error === 'object' && 'code' in error && typeof error.code === 'string' ? error.code : ''; const reason = /^[a-z][a-z0-9_]{0,63}$/u.test(candidate) ? candidate : 'plugin_backend_unavailable'; return { status: 'unavailable' as const, pluginId, reason }; } })); return { library, installations: installations .filter((installation): installation is NonNullable => installation !== null) .sort((left, right) => left.pluginId.localeCompare(right.pluginId)), }; } export function createCodingPluginMarketplaceService( options: CreateCodingPluginMarketplaceServiceOptions, ): CodingPluginMarketplaceService { const install = async (pluginId: string, kind: 'install' | 'update'): Promise => { const snapshot = await options.packageStore.resolveAndInstall({ pluginId, makeloreVersion: options.clientVersion, channel: 'stable', }); await options.onChanged?.({ pluginId, kind }); return publicInstallation(snapshot); }; const installBeta = async (pluginId: string): Promise => { const snapshot = await options.packageStore.resolveAndInstall({ pluginId, makeloreVersion: options.clientVersion, channel: 'beta', explicitBeta: true, }); await options.onChanged?.({ pluginId, kind: 'install' }); return publicInstallation(snapshot); }; const uninstall = async (pluginId: string): Promise => { const snapshot = await options.packageStore.uninstall(pluginId); await options.onChanged?.({ pluginId, kind: 'uninstall' }); return publicInstallation(snapshot); }; return { readCatalog: (input = {}) => options.marketplace.readCatalog(input), readDetail: (pluginId) => options.marketplace.readDetail(pluginId), readLibrary: async () => publicLibrary(await options.marketplace.readLibrary(), options.packageStore), acquire: async (pluginId) => { const snapshot = await options.marketplace.acquire(pluginId); await options.onChanged?.({ pluginId, kind: 'acquire' }); return publicLibrary(snapshot, options.packageStore); }, remove: async (pluginId) => { const snapshot = await options.marketplace.remove(pluginId); await options.onChanged?.({ pluginId, kind: 'remove' }); return publicLibrary(snapshot, options.packageStore); }, install: (pluginId) => install(pluginId, 'install'), installBeta, update: (pluginId) => install(pluginId, 'update'), uninstall, }; } 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; unknownPluginIds: string[]; 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[]; getDefinitions?(): readonly CodingPluginDefinition[] | Promise; effectiveResolver?: EffectivePluginResolver; } 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 adapters = new Map(options.adapters.map((adapter) => [adapter.pluginId, adapter])); async function definitions(): Promise { const result = new Map(options.definitions.map((definition) => [definition.id, definition])); for (const definition of options.getDefinitions ? await options.getDefinitions() : []) { // Bundled definitions are code-owned and cannot be shadowed by an // installed package reusing their identifier. if (!result.has(definition.id)) result.set(definition.id, definition); } return [...result.values()]; } 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); const pluginDefinitions = await definitions(); if (pluginDefinitions.some(({ requiresBackend }) => requiresBackend)) { await options.policyClient.refresh(); } const policy = options.policyClient.getState(); const [selection, config] = await Promise.all([ options.projectPlugins.getEnabledPluginIds(localProject.path), readCodingProjectConfigV2(localProject.path), ]); const assignedSkillIds = config.status === 'valid' ? [...new Set(config.config.agents .filter((agent) => agent.enabled && !agent.archivedAt) .flatMap(({ skillIds }) => skillIds))] : []; const effective = options.effectiveResolver ? await options.effectiveResolver.resolve({ projectId: localProject.id, projectPath: localProject.path, assignedSkillIds, role: 'parent', }) : null; const durableProjectId = config.status === 'valid' ? config.config.projectId ?? null : null; const items = await Promise.all(pluginDefinitions.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)) : definition.requiresBackend ? degradedBackend() : { status: 'not_required' }; } catch { backend = degradedBackend(); } const pluginPolicy = policy.catalog?.plugins.find(({ plugin_id }) => plugin_id === definition.id); const policyAvailable = !definition.requiresBackend || 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 : []; const effectiveReason = effective?.unavailableReasons.find(({ pluginId }) => pluginId === definition.id); const hasEffectiveSkill = effective?.effectiveSkillIds.some((skillId) => ( definition.skills.some(({ id }) => id === skillId) )) ?? false; let state = effectiveState({ enabled, policyAvailable, backend }); if (effective && effectiveReason?.code === 'project_disabled') state = 'disabled'; else if (effective && !hasEffectiveSkill && effectiveReason && ['account_required', 'library_required', 'library_unavailable', 'release_not_installed', 'release_invalid', 'client_incompatible', 'skill_owner_conflict', 'runtime_suspended', 'policy_unavailable', 'policy_unsupported'] .includes(effectiveReason.code)) state = 'unavailable'; return { id: definition.id, version: definition.version, contractVersion: definition.contractVersion, requiresBackend: definition.requiresBackend, displayName: definition.displayName, description: definition.description, enabled, state, 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, unknownPluginIds: [...new Set(selection.filter((id) => ( !pluginDefinitions.some((definition) => definition.id === id) )))].sort(), 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; effectiveResolver?: EffectivePluginResolver; 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 assignedSkillIds = await selectedSkillIds(project.path, agentId); const effective = agentId && options.effectiveResolver ? await options.effectiveResolver.resolve({ projectId: project.id, projectPath: project.path, assignedSkillIds, role: 'parent', }) : null; // Keep the raw assignment for the Renderer projection so a known // disabled Skill remains visible as selected/ineffective. The resolver // still supplies the availability set; only the worker opener receives // the effective subset. const skillIds = assignedSkillIds; // The unscoped list is the Agent assignment catalog. It must expose // every project-enabled plugin Skill before that Skill has been assigned; // the resolver's effective subset is only authoritative once an Agent is // selected (and for worker-facing projections). const enabledPluginIds = !agentId && options.getEnabledPluginIds ? await options.getEnabledPluginIds(project.path) : effective ? effective.effectiveSkillIds : options.getEnabledPluginIds ? await options.getEnabledPluginIds(project.path) : []; return await options.productTools.listSkills( skillIds, enabledPluginIds, ); }, async listCommands(conversationId) { const context = await conversationContext(conversationId); const effective = options.effectiveResolver ? await options.effectiveResolver.resolve({ projectId: context.project.id, projectPath: context.project.path, assignedSkillIds: context.skillIds, role: 'parent', }) : null; // Commands are projected from the assignment, while ProductTools marks // only resolver-approved Skills effective. This preserves disabled // assignment state without loading it into a worker. const skillIds = context.skillIds; const enabledPluginIds = effective ? effective.effectiveSkillIds : options.getEnabledPluginIds ? await options.getEnabledPluginIds(context.project.path) : []; const piCommands = options.listPiCommands ? normalizePiCommands(await options.listPiCommands(conversationId)) : []; return await options.productTools.listCommands(skillIds, piCommands, enabledPluginIds); }, async getChanges(conversationId) { await conversationContext(conversationId); return options.productTools.getChanges(conversationId); }, }; }