import path from 'node:path'; import type { CodingPluginDefinition, CodingPluginToolDefinition, } from '../../shared/coding-plugins'; import { isProjectWideCodingPluginId } from '../../shared/coding-plugins'; import { CORE_CODING_SKILL_IDS, type CodingSkillId, } from '../../shared/coding-skills'; import { accountBindingKey, type AccountBinding, type MarketplaceLibrarySnapshot, } from './account-plugin-cache'; import type { InstalledRelease } from './package-store'; import type { PluginBillingPolicy, PluginCatalog, PluginCatalogOperation, PluginPolicyClientState, } from '../services/plugin-policy-client'; /** The serialisable Skill resource consumed by every next Pi worker. */ export interface SkillEntry { readonly id: CodingSkillId; readonly entryPath: string; /** Main-verified package root for an installed Marketplace Skill. */ readonly packageRoot?: string; } /** A policy row copied from the last verified server catalog. */ export interface RuntimePolicy { readonly pluginId: string; readonly pluginVersion: string; readonly releaseId: string | null; readonly contractVersion: number; readonly capabilityId: string; readonly operation: string; readonly billing: PluginBillingPolicy; } export type PluginUnavailableReasonCode = | 'account_required' | 'library_required' | 'library_unavailable' | 'release_not_installed' | 'release_invalid' | 'client_incompatible' | 'project_disabled' | 'skill_unassigned' | 'skill_owner_conflict' | 'runtime_suspended' | 'policy_unavailable' | 'policy_unsupported' | 'billing_unavailable'; export interface PluginUnavailableReason { readonly pluginId: string; readonly code: PluginUnavailableReasonCode; readonly message: string; } /** * Main-owned, frozen worker contract. No renderer or worker state is an * authority: all consumers receive this exact projection for one worker. */ export interface EffectivePluginSnapshot { readonly accountSessionId: string; readonly projectId: string; readonly pluginReleaseIds: readonly string[]; readonly effectiveSkillIds: readonly CodingSkillId[]; readonly skillEntries: readonly SkillEntry[]; readonly toolDefinitions: readonly CodingPluginToolDefinition[]; readonly runtimePolicies: readonly RuntimePolicy[]; readonly unavailableReasons: readonly PluginUnavailableReason[]; } export interface EffectivePluginResolverInput { readonly projectId: string; readonly projectPath: string; readonly assignedSkillIds: readonly string[]; readonly role: 'parent' | 'child'; /** Test and composition seam for an already fetched Account Library. */ readonly library?: MarketplaceLibrarySnapshot | null; } export interface EffectivePluginResolverOptions { readonly definitions?: readonly CodingPluginDefinition[]; readonly getDefinitions?: () => readonly CodingPluginDefinition[] | Promise; readonly installedDefinitions?: readonly CodingPluginDefinition[]; readonly getInstalled?: (pluginId: string) => Promise | InstalledRelease | null; readonly packageStore?: { readInstalledIndex(): Promise; getInstalled(pluginId: string): Promise; getInstalledRelease(pluginId: string, releaseId: string): Promise; }; readonly getLibrary?: (binding: AccountBinding) => Promise | MarketplaceLibrarySnapshot | null; readonly marketplace?: { readLibrary(): Promise; }; readonly accountCache?: { getLibrary(binding: AccountBinding): MarketplaceLibrarySnapshot | null; }; readonly getAccountBinding?: () => AccountBinding | null; readonly getAccountSessionId?: () => string; readonly getEnabledPluginIds?: (projectPath: string) => Promise; readonly projectPlugins?: { getEnabledPluginIds(projectPath: string): Promise; }; readonly policyClient?: { getState(): PluginPolicyClientState; refresh(): Promise; }; } export interface EffectivePluginSkillSource { readonly id: string; readonly pluginId: string; readonly packageRoot: string; readonly directory: string; readonly entryPath: string; } interface DefinitionRecord { readonly definition: CodingPluginDefinition; readonly installed: boolean; readonly unavailableReason?: PluginUnavailableReasonCode; } const EMPTY_POLICY_STATE: PluginPolicyClientState = { status: 'unavailable', catalog: null, revision: 0, lastVerifiedAt: null, }; function clone(value: T): T { return structuredClone(value); } function freezeArray(value: readonly T[]): readonly T[] { return Object.freeze([...value]); } function deepFreeze(value: T): T { if (value && typeof value === 'object' && !Object.isFrozen(value)) { for (const child of Object.values(value as Record)) deepFreeze(child); Object.freeze(value); } return value; } function freezeSnapshot(value: EffectivePluginSnapshot): EffectivePluginSnapshot { const effectiveSkillIds = [...new Set(value.effectiveSkillIds)]; const skillEntries = [...new Map(value.skillEntries.map((entry) => [entry.id, entry])).values()]; return Object.freeze({ accountSessionId: value.accountSessionId, projectId: value.projectId, pluginReleaseIds: freezeArray(value.pluginReleaseIds), effectiveSkillIds: freezeArray(effectiveSkillIds), skillEntries: freezeArray(skillEntries.map((entry) => Object.freeze({ ...entry }))), toolDefinitions: freezeArray(value.toolDefinitions.map((tool) => deepFreeze(clone(tool)))), runtimePolicies: freezeArray(value.runtimePolicies.map((policy) => deepFreeze({ ...policy, billing: clone(policy.billing), }))), unavailableReasons: freezeArray(value.unavailableReasons.map((reason) => Object.freeze({ ...reason }))), }); } function accountSessionId( options: EffectivePluginResolverOptions, binding: AccountBinding | null, ): string { const provided = options.getAccountSessionId?.(); if (provided && provided.trim()) return provided; return binding ? accountBindingKey(binding) : 'anonymous'; } function normalizeIds(value: readonly string[]): string[] { return [...new Set(value.map((id) => id.trim()).filter(Boolean))]; } function policyOperation( catalog: PluginCatalog | null, definition: CodingPluginDefinition, capabilityId: string, operation: string, ): 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; return plugin.capabilities .find(({ capability_id }) => capability_id === capabilityId) ?.operations.find((candidate) => candidate.operation === operation) ?? null; } function policyIsBillingAvailable(policy: PluginCatalogOperation): boolean { return policy.billing.mode !== 'platform_metered' || !('status' in policy.billing) || policy.billing.status !== 'billing_unavailable'; } function unavailable( pluginId: string, code: PluginUnavailableReasonCode, message: string, ): PluginUnavailableReason { return { pluginId, code, message }; } function validateBinding(value: AccountBinding | null): AccountBinding | null { if (!value || typeof value.accountKey !== 'string' || value.accountKey.length === 0 || value.accountKey.length > 512 || !Number.isSafeInteger(value.epoch) || value.epoch < 0) { return null; } return { accountKey: value.accountKey, epoch: value.epoch }; } /** * A raw Skill ID is the public assignment key. Marketplace packages cannot * shadow a core Skill or an already accepted package owner; the project * assignment is retained, but the later package contributes no resources. */ function marketplaceSkillConflicts(records: readonly DefinitionRecord[]): ReadonlySet { const blocked = new Set(); const owned = new Set(CORE_CODING_SKILL_IDS); for (const { definition } of records) { if (definition.provenance.source === 'marketplace') continue; for (const { id } of definition.skills) owned.add(id); } for (const { definition } of records) { if (definition.provenance.source !== 'marketplace') continue; const conflicts = definition.skills.some(({ id }) => owned.has(id)); if (conflicts) { blocked.add(definition.id); continue; } for (const { id } of definition.skills) owned.add(id); } return blocked; } function selectedSkillsForDefinition( definition: CodingPluginDefinition, assignedSkillIds: readonly string[], ): CodingPluginDefinition['skills'] { return isProjectWideCodingPluginId(definition.id) ? definition.skills : definition.skills.filter(({ id }) => assignedSkillIds.includes(id)); } /** * Resolve a worker snapshot from separated Marketplace, Package Store, * project, assignment, and policy state. This module deliberately performs * no package execution and never trusts paths supplied by a package. */ export class EffectivePluginResolver { private readonly cachedLibraries = new Map(); constructor(private readonly options: EffectivePluginResolverOptions) {} async resolve(input: EffectivePluginResolverInput): Promise { return this.resolveSnapshot(input, false); } /** * Revalidate an already frozen worker before invoking one of its tools. * A stale Library snapshot is still the last verified account projection; * the hosted adapter resolves a fresh server Admission for the exact frozen * Release before dispatch. New workers continue to require a current Library. */ async resolveForInvocation(input: EffectivePluginResolverInput): Promise { return this.resolveSnapshot(input, true); } private async resolveSnapshot( input: EffectivePluginResolverInput, allowStaleHostedLibrary: boolean, ): Promise { const definitions = await this.definitionRecords(); const blockedMarketplacePlugins = marketplaceSkillConflicts(definitions); const assigned = normalizeIds(input.assignedSkillIds); const coreIds = new Set(CORE_CODING_SKILL_IDS); const effectiveSkillIds: CodingSkillId[] = assigned.filter((id) => coreIds.has(id)); const skillEntries: SkillEntry[] = effectiveSkillIds.map((id) => ({ id, entryPath: `${id}/SKILL.md`, })); const pluginReleaseIds: string[] = []; const toolDefinitions: CodingPluginToolDefinition[] = []; const runtimePolicies: RuntimePolicy[] = []; const unavailableReasons: PluginUnavailableReason[] = []; // Child workers intentionally receive only core resources. Still validate // assignment IDs above so malformed project configuration remains visible. if (input.role === 'child') { return freezeSnapshot({ accountSessionId: accountSessionId(this.options, validateBinding(this.options.getAccountBinding?.() ?? null)), projectId: input.projectId, pluginReleaseIds, effectiveSkillIds, skillEntries, toolDefinitions, runtimePolicies, unavailableReasons, }); } const binding = validateBinding(this.options.getAccountBinding?.() ?? null); const userDefinitions = definitions.filter(({ definition }) => definition.acquisitionMode === 'user_acquired'); const enabled = new Set(await this.enabledPluginIds(input.projectPath)); const requiresLibrary = userDefinitions.some(({ definition, installed }) => ( installed && enabled.has(definition.id) && selectedSkillsForDefinition(definition, assigned).length > 0 )); const library = input.library !== undefined ? input.library : await this.libraryFor(binding, requiresLibrary); const libraryById = new Map((library?.items ?? []).map((entry) => [entry.pluginId, entry])); let policyState: PluginPolicyClientState = this.options.policyClient?.getState() ?? EMPTY_POLICY_STATE; const serverDefinitions = definitions.filter(({ definition }) => definition.requiresBackend); const needsPolicyRefresh = policyState.status !== 'current' && serverDefinitions.some(({ definition, installed }) => ( installed && enabled.has(definition.id) && selectedSkillsForDefinition(definition, assigned).length > 0 )); if (needsPolicyRefresh && this.options.policyClient) { await this.options.policyClient.refresh(); policyState = this.options.policyClient.getState(); } for (const { definition, installed, unavailableReason } of definitions) { if (blockedMarketplacePlugins.has(definition.id)) { unavailableReasons.push(unavailable( definition.id, 'skill_owner_conflict', 'Plugin Skill ID conflicts with an existing owner', )); continue; } const selectedSkills = selectedSkillsForDefinition(definition, assigned); if (selectedSkills.length === 0) { unavailableReasons.push(unavailable(definition.id, 'skill_unassigned', 'Plugin Skill is not assigned')); continue; } if (!installed) { unavailableReasons.push(unavailable(definition.id, 'release_not_installed', 'Plugin Release is not installed')); continue; } if (unavailableReason) { unavailableReasons.push(unavailable( definition.id, unavailableReason, unavailableReason === 'client_incompatible' ? 'Plugin Release is incompatible with this MakeLore client' : 'Plugin Release is unavailable', )); continue; } if (!enabled.has(definition.id)) { unavailableReasons.push(unavailable(definition.id, 'project_disabled', 'Plugin is not enabled for this project')); continue; } if (definition.acquisitionMode === 'user_acquired') { if (!binding) { unavailableReasons.push(unavailable(definition.id, 'account_required', 'Marketplace account is required')); continue; } if (!library || (library.stale && definition.runtimeKind !== 'skill_only' && !allowStaleHostedLibrary)) { unavailableReasons.push(unavailable(definition.id, 'library_unavailable', 'Marketplace Library is unavailable')); continue; } const entry = libraryById.get(definition.id); if (!entry || entry.acquisitionMode !== 'user_acquired' || entry.removedAt !== null) { unavailableReasons.push(unavailable(definition.id, 'library_required', 'Plugin is not acquired in this Account Library')); continue; } // Retired remains usable for an existing Library user. Suspension is // a runtime gate and is intentionally independent from catalogStatus. if (entry.runtimeStatus === 'suspended') { unavailableReasons.push(unavailable(definition.id, 'runtime_suspended', 'Plugin runtime is suspended')); continue; } if (!definition.releaseId) { unavailableReasons.push(unavailable(definition.id, 'release_invalid', 'Installed Plugin Release is invalid')); continue; } } if (definition.requiresBackend) { const pluginPolicy = policyState.catalog?.plugins.find(({ plugin_id }) => plugin_id === definition.id); if (policyState.status !== 'current' || !policyState.catalog) { unavailableReasons.push(unavailable(definition.id, 'policy_unavailable', 'Plugin runtime policy is unavailable')); continue; } if (!pluginPolicy || !pluginPolicy.supported_contract_versions.includes(definition.contractVersion)) { unavailableReasons.push(unavailable(definition.id, 'policy_unsupported', 'Plugin contract is not supported by runtime policy')); continue; } } if (definition.releaseId) pluginReleaseIds.push(definition.releaseId); 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 } : {}), }); } if (!definition.requiresBackend) continue; for (const operation of definition.operations) { const policy = policyOperation( policyState.catalog, definition, operation.capabilityId, operation.operation, ); if (!policy) continue; runtimePolicies.push({ pluginId: definition.id, pluginVersion: definition.version, releaseId: definition.releaseId, contractVersion: definition.contractVersion, capabilityId: operation.capabilityId, operation: operation.operation, billing: clone(policy.billing), }); const tool = operation.toolName ? definition.tools.find(({ name }) => name === operation.toolName) : undefined; if (!tool || !selectedSkills.some(({ grants }) => grants.includes(tool.capabilityId))) continue; if (!policyIsBillingAvailable(policy)) { unavailableReasons.push(unavailable(definition.id, 'billing_unavailable', 'Plugin billing is unavailable')); continue; } toolDefinitions.push(clone(tool)); } } return freezeSnapshot({ accountSessionId: accountSessionId(this.options, binding), projectId: input.projectId, pluginReleaseIds: [...new Set(pluginReleaseIds)], effectiveSkillIds: [...new Set(effectiveSkillIds)], skillEntries, toolDefinitions: [...new Map(toolDefinitions.map((tool) => [tool.name, tool])).values()], runtimePolicies: [...new Map(runtimePolicies.map((policy) => [ `${policy.pluginId}\u0000${policy.capabilityId}\u0000${policy.operation}`, policy, ])).values()], unavailableReasons, }); } /** Alias kept explicit for callers that name the contract rather than the class. */ resolveEffectivePluginSnapshot(input: EffectivePluginResolverInput): Promise { return this.resolve(input); } async getSkillSources(): Promise { const definitions = await this.definitionRecords(); const blockedMarketplacePlugins = marketplaceSkillConflicts(definitions); // Bundled plugin Skills already have a trusted resource source supplied by // composition. Returning their manifest-relative `packageRoot` here would // replace that source with a cwd-relative path in the product projection; // this seam is exclusively for immutable user-installed package roots. return Object.freeze(definitions.flatMap(({ definition, installed, unavailableReason }) => ( !installed || unavailableReason || blockedMarketplacePlugins.has(definition.id) || definition.provenance.source !== 'marketplace' ? [] : definition.skills.map((skill) => ({ id: skill.id, pluginId: definition.id, packageRoot: definition.provenance.packageRoot, directory: path.join(definition.provenance.packageRoot, path.dirname(skill.entryPath)), entryPath: path.basename(skill.entryPath), })) ))); } getPolicyState(): PluginPolicyClientState { return this.options.policyClient?.getState() ?? EMPTY_POLICY_STATE; } async getInstalledDefinition( pluginId: string, releaseId?: string | null, ): Promise { const records = await this.definitionRecords(); const bundled = records.find(({ definition }) => ( definition.id === pluginId && definition.provenance.source === 'bundled' && definition.releaseId === releaseId )); if (bundled) { return bundled.installed && !bundled.unavailableReason ? bundled.definition : null; } if (releaseId && this.options.packageStore) { const installed = await this.options.packageStore.getInstalledRelease(pluginId, releaseId); return installed && !installed.unavailableReason ? installed.definition : null; } const record = records.find(({ definition }) => definition.id === pluginId); return record?.installed && !record.unavailableReason ? record.definition : null; } private async definitionRecords(): Promise { const base = [ ...(this.options.definitions ?? []), ...(this.options.getDefinitions ? await this.options.getDefinitions() : []), ]; const records = new Map(); for (const definition of base) { if (!records.has(definition.id)) records.set(definition.id, { definition, installed: true }); } for (const definition of this.options.installedDefinitions ?? []) { records.set(definition.id, { definition, installed: true }); } const installedIds = new Set(); if (this.options.packageStore) { const index = await this.options.packageStore.readInstalledIndex(); for (const record of index) installedIds.add(record.pluginId); } for (const definition of [...records.values()].map(({ definition }) => definition)) { if (definition.acquisitionMode !== 'user_acquired') continue; if (definition.provenance.source === 'bundled') continue; let installed = (this.options.installedDefinitions ?? []).some(({ id }) => id === definition.id); let unavailableReason: PluginUnavailableReasonCode | undefined; if (installedIds.has(definition.id)) { const installedRelease = await this.options.packageStore?.getInstalled(definition.id); installed = Boolean(installedRelease); if (installedRelease?.unavailableReason === 'plugin_incompatible_client') { unavailableReason = 'client_incompatible'; } } if (this.options.getInstalled) { const installedRelease = await this.options.getInstalled(definition.id); installed = Boolean(installedRelease); if (installedRelease?.unavailableReason === 'plugin_incompatible_client') { unavailableReason = 'client_incompatible'; } } if (!this.options.packageStore && !this.options.getInstalled && !(this.options.installedDefinitions ?? []).some(({ id }) => id === definition.id)) { installed = false; } records.set(definition.id, { definition, installed, unavailableReason }); } if (this.options.packageStore) { for (const pluginId of installedIds) { if (records.has(pluginId)) continue; const installed = await this.options.packageStore.getInstalled(pluginId); if (installed) records.set(pluginId, { definition: installed.definition, installed: true, ...(installed.unavailableReason === 'plugin_incompatible_client' ? { unavailableReason: 'client_incompatible' as const } : {}), }); } } return [...records.values()]; } private async libraryFor( binding: AccountBinding | null, required: boolean, ): Promise { if (!required || !binding) return null; const key = accountBindingKey(binding); const cached = this.options.accountCache?.getLibrary(binding) ?? this.cachedLibraries.get(key); if (cached && !cached.stale) this.cachedLibraries.set(key, clone(cached)); try { const library = this.options.getLibrary ? await this.options.getLibrary(binding) : this.options.marketplace ? await this.options.marketplace.readLibrary() : cached ?? null; if (library) this.cachedLibraries.set(key, clone(library)); return library ?? cached ?? null; } catch { return cached ?? null; } } private async enabledPluginIds(projectPath: string): Promise { if (this.options.getEnabledPluginIds) return await this.options.getEnabledPluginIds(projectPath); if (this.options.projectPlugins) return await this.options.projectPlugins.getEnabledPluginIds(projectPath); return []; } } export function createEffectivePluginResolver( options: EffectivePluginResolverOptions, ): EffectivePluginResolver { return new EffectivePluginResolver(options); } export const createEffectiveResolver = createEffectivePluginResolver; export const resolveEffectivePluginSnapshot = async ( options: EffectivePluginResolverOptions, input: EffectivePluginResolverInput, ): Promise => await createEffectivePluginResolver(options).resolve(input);