/** * A module that carries convergence code nothing ever runs * (openspec/changes/module-integrity-rigor, D10; celilo#934). * * No convergence framework. celilo already has one — `subscriptions:` plus * `timer.tick.`, driven by the event bus, used by `technitium`, * `wireguard-manager`, `namecheap` and `celilo-mgmt`. celilo#934 asked whether * drift detection and correction deserve a shared shape; they have one. The * gap was that nothing noticed when a module had the shape and not the wiring, * so `wireguard` computed peer drift and corrected none of it. * * **What this rule actually checks, and how it narrows D10.** The design says * "a module whose health check computes drift and whose manifest declares no * reconcile subscription". The first half is not decidable from a manifest — * celilo cannot read a hook script and tell whether it computes drift — so * this checks the decidable, strictly stronger half: a module that DECLARES a * convergence hook which no subscription fires. That names dead machinery with * no interpretation and no false positives. A module that detects drift and * declares no correcting hook at all is a missing feature, and a rule that * guessed at it from `health_check` alone would flag most of the fleet. */ import type { ModuleManifest } from '../../manifest/schema'; import type { DriftFinding } from './types'; /** * Hooks whose whole purpose is to CORRECT something. If one of these exists * and nothing calls it, the module has the fix and never applies it. * * `list_peers` is deliberately absent: it reports what a tunnel carries and * corrects nothing, so it is a detection hook and firing it on a timer would * achieve nothing. */ const CONVERGENCE_HOOKS = [ 'reconcile_routes', 'reconcile_clients', 'reconcile_peers', 'refresh_registrations', 'reassert_dhcp_dns', ] as const; export interface DetectWithoutConvergeAuditDeps { modules: { id: string; state: string; manifest: ModuleManifest }[]; } const DEPLOYED_STATES = new Set(['INSTALLED', 'VERIFIED']); export function auditDetectWithoutConverge(deps: DetectWithoutConvergeAuditDeps): DriftFinding[] { const findings: DriftFinding[] = []; for (const module of deps.modules) { if (!DEPLOYED_STATES.has(module.state)) continue; const hooks = module.manifest.hooks ?? {}; const declared = CONVERGENCE_HOOKS.filter((name) => hooks[name] !== undefined); if (declared.length === 0) continue; const fired = new Set( (module.manifest.subscriptions ?? []).map((s) => s.hook).filter((h): h is string => !!h), ); const unfired = declared.filter((name) => !fired.has(name)); if (unfired.length === 0) continue; findings.push({ category: 'detect_without_converge', severity: 'drift', code: 'converge_hook_never_fires', message: `${module.id}: declares ${unfired.join(', ')} but no subscription ever fires ${unfired.length === 1 ? 'it' : 'them'} — drift is detected and never corrected`, details: 'Add a `subscriptions:` entry on a `timer.tick.` event naming the hook. `technitium`, `wireguard-manager`, `namecheap` and `celilo-mgmt` all do this; the machinery exists and this module is not wired into it.', remediation: `Edit ${module.id}'s manifest.yml to add a subscriptions entry for ${unfired.join(', ')}, then 'celilo module update'.`, // Prose, not a command: the fix is an edit to a module's manifest, and // `actionable: true` on something no `celilo …` invocation performs would // put a Remediate button on a modal that cannot do anything. actionable: false, subject: module.id, }); } return findings; }