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
261 lines • 12.1 kB
JavaScript
/**
* V3 CLI Verify Command
*
* Fetches the verification.md.json witness manifest from the live repo,
* recomputes SHA-256 of every cited file in the user's installed
* artifact, re-derives the Ed25519 public key from the manifest's git
* commit, and verifies the signature.
*
* Run via: ruflo verify [--branch <branch>] [--manifest <local-path>]
*
* If everything checks, the user has byte-for-byte the same fix
* footprint as the manifest claims. If anything mismatches, the
* command exits non-zero and prints which fix regressed or which file
* drifted.
*/
import { createHash } from 'crypto';
import { existsSync, readFileSync } from 'fs';
import { dirname, join, sep } from 'path';
import { fileURLToPath } from 'url';
import { output } from '../output.js';
const DEFAULT_MANIFEST_URL = 'https://raw.githubusercontent.com/ruvnet/ruflo/{branch}/verification.md.json';
async function fetchWitness(branch) {
const url = DEFAULT_MANIFEST_URL.replace('{branch}', branch);
// audit_1776853149979: bare fetch had no timeout — a hung GitHub CDN would
// pin the verify command indefinitely. 30s is generous for a sub-MB JSON.
const res = await fetch(url, { signal: AbortSignal.timeout(30000) });
if (!res.ok) {
throw new Error(`Failed to fetch manifest from ${url}: ${res.status} ${res.statusText}`);
}
return await res.json();
}
function loadLocalWitness(localPath) {
if (!existsSync(localPath)) {
throw new Error(`Manifest not found: ${localPath}`);
}
return JSON.parse(readFileSync(localPath, 'utf-8'));
}
/**
* Locate the user's installed package root.
*
* The witness manifest paths are repo-relative (e.g.
* "v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js"). For
* end users, only the dist/ subtree ships in node_modules. We map
* the repo path → the installed equivalent by stripping the
* "v3/@claude-flow/<pkg>/" prefix and looking up node_modules/<pkg>/.
*/
function repoPathToInstalledPath(repoPath) {
// Match v3/@claude-flow/<pkg>/<rest>
const match = repoPath.match(/^v3\/(@claude-flow\/[^/]+)\/(.+)$/);
if (match) {
const pkg = match[1];
const rest = match[2];
const candidates = [];
// 1. cwd/node_modules/<pkg>/<rest> (typical end-user install)
candidates.push(join(process.cwd(), 'node_modules', pkg, rest));
// 2. Walk up from this script looking for node_modules/<pkg>/<rest>
// Covers cases where verify runs from inside a nested module.
try {
const __filename = fileURLToPath(import.meta.url);
let dir = dirname(__filename);
for (let i = 0; i < 10; i++) {
candidates.push(join(dir, 'node_modules', pkg, rest));
const parent = dirname(dir);
if (parent === dir)
break;
dir = parent;
}
}
catch { /* ignore */ }
// 3. Source-tree resolution: when verify runs against a checked-out
// repo (the developer's working copy), packages live at
// `<repoRoot>/v3/<pkg>/<rest>` rather than under node_modules.
// Walk up looking for the literal repo-relative path so the verify
// command works for maintainers running it from the repo itself.
try {
const __filename = fileURLToPath(import.meta.url);
let dir = dirname(__filename);
for (let i = 0; i < 10; i++) {
candidates.push(join(dir, repoPath));
const parent = dirname(dir);
if (parent === dir)
break;
dir = parent;
}
}
catch { /* ignore */ }
candidates.push(join(process.cwd(), repoPath));
for (const c of candidates) {
if (existsSync(c))
return c;
}
return null;
}
// Top-level paths (e.g. package.json) — return relative to cwd
const top = join(process.cwd(), repoPath);
return existsSync(top) ? top : null;
}
function fileSha256(path) {
return createHash('sha256').update(readFileSync(path)).digest('hex');
}
function fileContains(path, marker) {
return readFileSync(path, 'utf-8').includes(marker);
}
async function verifySignature(witness) {
// Lazy-load @noble/ed25519 — keep verify command snappy when no signature check needed
let ed = null;
try {
ed = await import('@noble/ed25519');
}
catch {
return { manifestHashOk: false, publicKeyReproducible: false, signatureValid: false };
}
// Configure sync sha512 for the v2 API
const sha512Sync = (...m) => {
const h = createHash('sha512');
for (const x of m)
h.update(x);
return h.digest();
};
ed.etc.sha512Sync = sha512Sync;
const manifestCanonical = JSON.stringify(witness.manifest);
const recomputedHash = createHash('sha256').update(manifestCanonical).digest('hex');
const manifestHashOk = recomputedHash === 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 };
}
export const verifyCommand = {
name: 'verify',
description: 'Verify installed artifact against the signed witness manifest',
options: [
{
name: 'branch',
short: 'b',
type: 'string',
description: 'Git branch to fetch verification.md.json from (defaults to fix/issues-may-1-3)',
default: 'fix/issues-may-1-3',
},
{
name: 'manifest',
short: 'm',
type: 'string',
description: 'Local path to a verification.md.json file (overrides --branch)',
},
{
name: 'json',
type: 'boolean',
description: 'Output JSON instead of human-readable table',
default: false,
},
],
examples: [
{ command: 'ruflo verify', description: 'Fetch latest manifest from main branch + verify' },
{ command: 'ruflo verify --branch main', description: 'Verify against a specific branch' },
{ command: 'ruflo verify --manifest ./verification.md.json', description: 'Use a local manifest copy' },
{ command: 'ruflo verify --json', description: 'Machine-readable output for CI' },
],
action: async (ctx) => {
const branch = ctx.flags.branch || 'fix/issues-may-1-3';
const localPath = ctx.flags.manifest;
const asJson = ctx.flags.json === true;
if (!asJson) {
output.writeln();
output.writeln(output.bold('Ruflo Verification'));
output.writeln(output.dim('─'.repeat(50)));
}
let witness;
try {
witness = localPath ? loadLocalWitness(localPath) : await fetchWitness(branch);
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err);
if (asJson)
output.printJson({ ok: false, error: msg });
else
output.printError(`Could not load witness manifest: ${msg}`);
return { success: false, exitCode: 1 };
}
// Signature verification
const sig = await verifySignature(witness);
// File verification
const fileResults = witness.manifest.fixes.map((fix) => {
const installedPath = repoPathToInstalledPath(fix.file);
if (!installedPath) {
return { ...fix, status: 'missing', sha256Match: false, markerPresent: false, installedPath: null, localSha256: undefined };
}
const localHash = fileSha256(installedPath);
const markerPresent = fileContains(installedPath, fix.marker);
const sha256Match = localHash === fix.sha256;
const status = sha256Match && markerPresent
? 'pass'
: (markerPresent ? 'drift' : 'regressed');
return { ...fix, status, sha256Match, markerPresent, localSha256: localHash, installedPath: installedPath.replace(process.cwd() + sep, '') };
});
const passCount = fileResults.filter((r) => r.status === 'pass').length;
const driftCount = fileResults.filter((r) => r.status === 'drift').length;
const regressedCount = fileResults.filter((r) => r.status === 'regressed').length;
const missingCount = fileResults.filter((r) => r.status === 'missing').length;
const allOk = sig.manifestHashOk && sig.publicKeyReproducible && sig.signatureValid && regressedCount === 0;
if (asJson) {
output.printJson({
ok: allOk,
manifest: witness.manifest,
signature: sig,
results: fileResults,
summary: { pass: passCount, drift: driftCount, regressed: regressedCount, missing: missingCount },
});
return { success: allOk, exitCode: allOk ? 0 : 1 };
}
output.writeln();
output.writeln(output.bold('Manifest signature'));
output.writeln(` manifest hash matches: ${sig.manifestHashOk ? output.success('yes') : output.error('no')}`);
output.writeln(` public key reproducible from gitCommit: ${sig.publicKeyReproducible ? output.success('yes') : output.error('no')}`);
output.writeln(` Ed25519 signature valid: ${sig.signatureValid ? output.success('yes') : output.error('no')}`);
output.writeln();
output.writeln(output.bold('Fix verification'));
for (const r of fileResults) {
const status = r.status === 'pass'
? output.success('pass')
: r.status === 'drift'
? output.warning('drift')
: output.error(r.status);
output.writeln(` [${status}] ${r.id} — ${r.desc}`);
if (r.status === 'drift' && r.localSha256) {
output.writeln(output.dim(` expected sha256: ${r.sha256.slice(0, 16)}…`));
output.writeln(output.dim(` local sha256: ${r.localSha256.slice(0, 16)}…`));
}
else if (r.status === 'regressed') {
output.writeln(output.dim(` marker missing: '${r.marker}' not found in ${r.installedPath ?? r.file}`));
}
else if (r.status === 'missing') {
output.writeln(output.dim(` file not found in node_modules: ${r.file}`));
}
}
output.writeln();
output.writeln(output.bold('Summary'));
output.writeln(` pass: ${passCount}`);
output.writeln(` drift: ${driftCount}`);
output.writeln(` regressed: ${regressedCount}`);
output.writeln(` missing: ${missingCount}`);
output.writeln();
if (allOk) {
output.printSuccess('All fixes verified. Installed artifact matches the signed witness manifest.');
return { success: true };
}
if (regressedCount > 0) {
output.printError(`${regressedCount} fix(es) regressed. Markers not found in installed artifact.`);
}
if (driftCount > 0) {
output.printWarning(`${driftCount} fix(es) drifted. Markers present, but file SHA-256 differs (could be a benign edit; inspect the diff).`);
}
if (!sig.signatureValid || !sig.manifestHashOk) {
output.printError('Manifest signature failed verification. The witness file may have been tampered with or corrupted.');
}
return { success: false, exitCode: 1 };
},
};
export default verifyCommand;
//# sourceMappingURL=verify.js.map