UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

156 lines (141 loc) 6.59 kB
#!/usr/bin/env node /** * Verify a signed witness manifest against the live tree (ADR-103). * * Project-agnostic — works without ruflo CLI being installed. * * Usage: * node verify.mjs --manifest <path> [--root <path>] [--json] * * Exit codes: * 0 — signature valid + all fixes pass or drift (marker present) * 1 — signature invalid OR any fix regressed/missing * 2 — bad arguments / file not found */ import { readFileSync, existsSync } from 'node:fs'; import { resolve, join, sep } from 'node:path'; import { createHash } from 'node:crypto'; import { createRequire } from 'node:module'; import { fileSha256, fileContains } from './lib.mjs'; const args = parseArgs(process.argv.slice(2)); if (!args.manifest) { console.error('--manifest <path> required'); process.exit(2); } const manifestPath = resolve(args.manifest); if (!existsSync(manifestPath)) { console.error(`not found: ${manifestPath}`); process.exit(2); } const repoRoot = resolve(args.root ?? process.cwd()); const asJson = !!args.json; const witness = JSON.parse(readFileSync(manifestPath, 'utf8')); // ─── signature ──────────────────────────────────────────────────── const sig = await verifySignature(witness, repoRoot); // ─── per-fix marker check ───────────────────────────────────────── const fileResults = witness.manifest.fixes.map((fix) => { const installed = join(repoRoot, fix.file); if (!existsSync(installed)) { return { ...fix, status: 'missing', sha256Match: false, markerPresent: false }; } const localSha256 = fileSha256(installed); const markerPresent = fileContains(installed, fix.marker); const sha256Match = localSha256 === fix.sha256; const status = sha256Match && markerPresent ? 'pass' : (markerPresent ? 'drift' : 'regressed'); return { ...fix, status, sha256Match, markerPresent, localSha256 }; }); const summary = { pass: fileResults.filter(r => r.status === 'pass').length, drift: fileResults.filter(r => r.status === 'drift').length, regressed: fileResults.filter(r => r.status === 'regressed').length, missing: fileResults.filter(r => r.status === 'missing').length, }; const ok = sig.signatureValid && sig.manifestHashOk && sig.publicKeyReproducible && summary.regressed === 0 && summary.missing === 0; if (asJson) { console.log(JSON.stringify({ ok, signature: sig, summary, results: fileResults }, null, 2)); } else { console.log('Manifest signature:'); console.log(` hash matches: ${sig.manifestHashOk ? 'yes' : 'NO'}`); console.log(` public key reproducible: ${sig.publicKeyReproducible ? 'yes' : 'NO'}`); console.log(` Ed25519 signature valid: ${sig.signatureValid ? 'yes' : 'NO'}`); console.log(''); console.log(`Summary: pass=${summary.pass} drift=${summary.drift} regressed=${summary.regressed} missing=${summary.missing}`); if (summary.regressed > 0) { console.log('\nRegressed:'); for (const r of fileResults.filter(r => r.status === 'regressed')) { console.log(` ${r.id} marker missing in ${r.file}`); } } if (summary.missing > 0) { console.log('\nMissing files:'); for (const r of fileResults.filter(r => r.status === 'missing')) { console.log(` ${r.id} ${r.file}`); } } } process.exit(ok ? 0 : 1); // ─── ed25519 helpers ───────────────────────────────────────────── async function verifySignature(witness, repoRoot) { // Probe multiple plausible install roots — pnpm's isolated linker // doesn't hoist transitive deps to v3/node_modules, so we also check // workspace packages that declare @noble/ed25519 directly. A user's // flat npm install satisfies the first probe; pnpm satisfies the latter. let ed; let probeErr; const probes = [ repoRoot, join(repoRoot, 'v3'), join(repoRoot, 'v3/@claude-flow/cli'), join(repoRoot, 'v3/@claude-flow/plugin-agent-federation'), ]; for (const root of probes) { try { ed = createRequire(join(root, 'noop.js'))('@noble/ed25519'); break; } catch (e) { probeErr = e; } } if (!ed) { // ruflo#1880 — the scheduled 12h verification has bounced off this // 6+ times. Spell out the fix in the error message instead of // leaving the operator to chase it. console.error( `verify.mjs: could not load @noble/ed25519 from any of:\n` + ` ${probes.join('\n ')}\n` + ` last error: ${probeErr?.message ?? '?'}\n` + `\n` + `Fix: from the repo root, run \`npm install\` (the dep is declared\n` + `in the root package.json under @noble/ed25519). If your runner\n` + `is a source-only checkout, your verification pipeline must run\n` + `\`npm ci && npm run build\` before invoking this script. See #1880\n` + `for the full diagnosis.` ); return { manifestHashOk: false, publicKeyReproducible: false, signatureValid: false, // Machine-parseable hint for the scheduled runner so it can // distinguish "missing dep" from a real signature failure. reason: 'noble-ed25519-not-installed', }; } ed.etc.sha512Sync = (...m) => { const h = createHash('sha512'); for (const x of m) h.update(x); return h.digest(); }; const recomputed = createHash('sha256').update(JSON.stringify(witness.manifest)).digest('hex'); const manifestHashOk = recomputed === witness.integrity.manifestHash; const seed = createHash('sha256').update(witness.manifest.gitCommit + ':ruflo-witness/v1').digest(); const reKey = ed.getPublicKey(seed); const publicKeyReproducible = Buffer.from(reKey).toString('hex') === witness.integrity.publicKey; const signatureValid = ed.verify( Buffer.from(witness.integrity.signature, 'hex'), Buffer.from(witness.integrity.manifestHash, 'hex'), Buffer.from(witness.integrity.publicKey, 'hex'), ); return { manifestHashOk, publicKeyReproducible, signatureValid }; } function parseArgs(argv) { const out = {}; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a === '--json' || a === '--help') { out[a.slice(2)] = true; continue; } if (a.startsWith('--')) { const key = a.slice(2); const next = argv[i + 1]; if (next && !next.startsWith('--')) { out[key] = next; i++; } else { out[key] = true; } } } return out; }