/** * Capability ABI compatibility check (CELILO_UPDATE D4). * * For each installed module, compares the version it claims for each * capability (in `provides` / `requires`) against: * - the framework's runtime registry (`CAPABILITY_CONTRACT_VERSIONS`), * for providers * - the actually-deployed provider's claimed version, for consumers * * Mismatches produce `blocked` findings with concrete remediation * (rebuild the provider, upgrade the consumer, etc.). */ import { CAPABILITY_CONTRACT_VERSIONS, type KnownCapabilityName, compareConsumerToProvider, compareProviderToRuntime, } from '@celilo/capabilities'; import type { CapabilityProvider, CapabilityRequirement, ModuleManifest, } from '../../manifest/schema'; import type { DriftFinding } from './types'; export interface InstalledCapabilityModule { id: string; /** * Lifecycle state — used to demote ABI mismatches on IMPORTED-but- * not-deployed modules from `blocked` to `todo`. The mismatch is * still real (the operator will hit it at deploy time) but it * doesn't block other module work, and gating system update on it * makes refreshing unrelated modules harder than it should be. */ state: string; manifest: ModuleManifest; } const DEPLOYED_STATES = new Set(['INSTALLED', 'VERIFIED']); export interface CapabilityAbiAuditDeps { modules: InstalledCapabilityModule[]; /** Override the framework registry — for tests. Defaults to `CAPABILITY_CONTRACT_VERSIONS`. */ contractVersions?: Record; } function isKnownCapability(name: string): name is KnownCapabilityName { return name in CAPABILITY_CONTRACT_VERSIONS; } interface ProviderClaim { capability: string; version: string; moduleId: string; } function collectProviders(modules: InstalledCapabilityModule[]): Map { // capability name → { module that provides it, version it claims }. // If multiple modules provide the same capability, the first one wins // for the audit's consumer-vs-provider check; the framework's // capability-loader handles real provider selection elsewhere. const map = new Map(); for (const m of modules) { const provides: CapabilityProvider[] = m.manifest.provides?.capabilities ?? []; for (const p of provides) { if (!map.has(p.name)) { map.set(p.name, { capability: p.name, version: p.version, moduleId: m.id }); } } } return map; } export async function auditCapabilityAbi(deps: CapabilityAbiAuditDeps): Promise { const findings: DriftFinding[] = []; const contractVersions = deps.contractVersions ?? CAPABILITY_CONTRACT_VERSIONS; const providers = collectProviders(deps.modules); // 1. Provider checks: each `provides[X].version` must match the // framework's runtime registry (within compareProviderToRuntime's rules). for (const m of deps.modules) { const provides: CapabilityProvider[] = m.manifest.provides?.capabilities ?? []; for (const p of provides) { if (!isKnownCapability(p.name)) { // Capability isn't registered in the framework — that's fine // (third-party / well-known-extension scenario). continue; } const runtimeVersion = contractVersions[p.name]; const result = compareProviderToRuntime(p.version, runtimeVersion); if (result.compatible) continue; const isFrameworkBehind = result.reason === 'major_mismatch_higher'; const details = isFrameworkBehind ? [ 'ABI = Application Binary Interface — the contract between', 'the celilo framework and a module for a given capability', '(method names, argument shapes, return values).', '', `${m.id} expects ${p.name}@${p.version} but the running`, `framework only ships ${p.name}@${runtimeVersion}.`, '', 'To fix:', ' 1. Upgrade celilo: bun update -g @celilo/cli', ' (or `git pull && bun install` in this repo)', ' 2. Re-run system audit', '', 'This is a code-level mismatch — there is no single CLI', 'command that can resolve it.', ].join('\n') : [ 'ABI = Application Binary Interface — the contract between', 'the celilo framework and a module for a given capability', '(method names, argument shapes, return values).', '', `${m.id} was built against ${p.name}@${p.version} but the`, `framework now expects ${p.name}@${runtimeVersion}.`, '', 'To fix:', ` 1. cd modules/${m.id}`, ' 2. Update the @celilo/capabilities dep to match the runtime', ' 3. bun run build', ' 4. Republish (or reinstall locally with `module import`)', ' 5. Re-run system audit', '', 'This is a code-level mismatch — there is no single CLI', 'command that can resolve it.', ].join('\n'); findings.push({ category: 'capability_abi', // Demoted to `todo` for non-deployed modules: the mismatch // matters at deploy time, but blocking system update on it // means the operator can't refresh unrelated modules until // they fix the ABI for a module they haven't deployed yet. severity: DEPLOYED_STATES.has(m.state) ? 'blocked' : 'todo', code: 'capability_abi_provider_mismatch', message: `${m.id} provides ${p.name}@${p.version} but framework runtime expects ${runtimeVersion}`, details, actionable: false, subject: m.id, }); } } // 2. Consumer checks: each `requires[X].version` must be satisfied by // the actual provider's `provides[X].version`. for (const m of deps.modules) { const requires: CapabilityRequirement[] = m.manifest.requires?.capabilities ?? []; const optional: CapabilityRequirement[] = m.manifest.optional?.capabilities ?? []; for (const need of [...requires, ...optional]) { const provider = providers.get(need.name); if (!provider) { // No installed provider for this capability — that's a deploy-time // concern, not an ABI concern. The deploy preflight handles missing // providers; we skip here. continue; } const result = compareConsumerToProvider(need.version, provider.version); if (result.compatible) continue; const providerTooOld = result.reason === 'caller_minor_too_old'; const details = providerTooOld ? [ 'ABI = Application Binary Interface — the contract between', 'modules for a given capability (method names, argument', 'shapes, return values).', '', `${m.id} requires ${need.name}@${need.version} but the`, `installed provider ${provider.moduleId} only offers`, `${provider.version}.`, '', 'To fix:', ` 1. Upgrade ${provider.moduleId} to a version that provides`, ` ${need.name}@${need.version} or newer (compatible major)`, ' 2. Re-run system audit', '', 'This is a code-level mismatch — there is no single CLI', 'command that can resolve it.', ].join('\n') : [ 'ABI = Application Binary Interface — the contract between', 'modules for a given capability (method names, argument', 'shapes, return values).', '', `${m.id} was built against ${need.name}@${need.version} but`, `${provider.moduleId} now provides ${provider.version} on a`, 'different major. The interface has changed in an', 'incompatible way.', '', 'To fix:', ` 1. cd modules/${m.id}`, ` 2. Update its requires entry for ${need.name} to match`, ` ${provider.moduleId}'s major (${provider.version})`, ' 3. Adjust the module code if the interface changed', ' 4. bun run build && republish', ' 5. Re-run system audit', '', 'This is a code-level mismatch — there is no single CLI', 'command that can resolve it.', ].join('\n'); findings.push({ category: 'capability_abi', // Same demotion as the provider check — non-deployed // modules surface as todos rather than blockers. severity: DEPLOYED_STATES.has(m.state) ? 'blocked' : 'todo', code: 'capability_abi_consumer_mismatch', message: `${m.id} requires ${need.name}@${need.version} but ${provider.moduleId} provides ${provider.version}`, details, actionable: false, subject: m.id, }); } } return findings; }