Files
makelore/electron/coding-plugins/effective-resolver.ts

546 lines
22 KiB
TypeScript

import path from 'node:path';
import type {
CodingPluginDefinition,
CodingPluginToolDefinition,
} 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 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'
| '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 CodingPluginDefinition[]>;
readonly installedDefinitions?: readonly CodingPluginDefinition[];
readonly getInstalled?: (pluginId: string) => Promise<InstalledRelease | null> | InstalledRelease | null;
readonly packageStore?: {
readInstalledIndex(): Promise<readonly { pluginId: string }[]>;
getInstalled(pluginId: string): Promise<InstalledRelease | null>;
};
readonly getLibrary?: (binding: AccountBinding) => Promise<MarketplaceLibrarySnapshot | null> | MarketplaceLibrarySnapshot | null;
readonly marketplace?: {
readLibrary(): Promise<MarketplaceLibrarySnapshot>;
};
readonly accountCache?: {
getLibrary(binding: AccountBinding): MarketplaceLibrarySnapshot | null;
};
readonly getAccountBinding?: () => AccountBinding | null;
readonly getAccountSessionId?: () => string;
readonly getEnabledPluginIds?: (projectPath: string) => Promise<readonly string[]>;
readonly projectPlugins?: {
getEnabledPluginIds(projectPath: string): Promise<readonly string[]>;
};
readonly policyClient?: {
getState(): PluginPolicyClientState;
refresh(): Promise<unknown>;
};
}
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<T>(value: T): T {
return structuredClone(value);
}
function freezeArray<T>(value: readonly T[]): readonly T[] {
return Object.freeze([...value]);
}
function deepFreeze<T>(value: T): T {
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
for (const child of Object.values(value as Record<string, unknown>)) 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<string> {
const blocked = new Set<string>();
const owned = new Set<string>(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;
}
/**
* 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<string, MarketplaceLibrarySnapshot>();
constructor(private readonly options: EffectivePluginResolverOptions) {}
async resolve(input: EffectivePluginResolverInput): Promise<EffectivePluginSnapshot> {
const definitions = await this.definitionRecords();
const blockedMarketplacePlugins = marketplaceSkillConflicts(definitions);
const assigned = normalizeIds(input.assignedSkillIds);
const coreIds = new Set<string>(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)
&& definition.skills.some(({ id }) => assigned.includes(id))
));
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) && definition.skills.some(({ id }) => assigned.includes(id))
));
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)) continue;
const selectedSkills = definition.skills.filter(({ id }) => assigned.includes(id));
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) {
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,
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<EffectivePluginSnapshot> {
return this.resolve(input);
}
async getSkillSources(): Promise<readonly EffectivePluginSkillSource[]> {
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.acquisitionMode !== 'user_acquired'
? []
: 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;
}
private async definitionRecords(): Promise<DefinitionRecord[]> {
const base = [
...(this.options.definitions ?? []),
...(this.options.getDefinitions ? await this.options.getDefinitions() : []),
];
const records = new Map<string, DefinitionRecord>();
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<string>();
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;
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<MarketplaceLibrarySnapshot | null> {
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<readonly string[]> {
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<EffectivePluginSnapshot> => await createEffectivePluginResolver(options).resolve(input);