Files
ruv fdeacad7f8 feat(metaharness): integrate @metaharness/darwin@0.3.1 + bump umbrella to 0.2.6
Implements ADR-153 (Darwin Mode integration). Adds the WRITE layer that
closes the loop ADR-150's READ layer opens: score/genome describe a
harness; evolve changes one.

Three new surfaces, all honoring the four ADR-150 architectural constraints
(removable / optional / graceful degradation / CI-absent-path coverage):

  - harness-evolve         mutate 1 of 7 policy surfaces, sandbox-score, promote
  - harness-security-bench upstream's "Darwin Shield" (their own ADR-155)
  - harness-bench          create/verify bench suites for evolve --bench

Wiring:
  - plugins/ruflo-metaharness/scripts/_darwin.mjs       — shared subprocess helper
  - plugins/ruflo-metaharness/scripts/evolve.mjs        — main verb (--confirm gate)
  - plugins/ruflo-metaharness/scripts/security-bench.mjs — Darwin Shield wrapper
  - plugins/ruflo-metaharness/scripts/bench.mjs         — supporting verb
  - plugins/ruflo-metaharness/skills/harness-{evolve,security-bench,bench}/SKILL.md
  - v3/@claude-flow/cli/src/mcp-tools/metaharness-tools.ts — 3 new MCP tools:
      metaharness_evolve, metaharness_security_bench, metaharness_bench
  - v3/@claude-flow/cli/package.json optionalDependencies:
      + @metaharness/darwin ~0.3.1
      bump metaharness ~0.1.11 -> ~0.2.6
  - ruflo/package.json optionalDependencies: mirrored (per the #2112 lesson —
      wrapper does NOT inherit root overrides; required for transitive pin)

Safety posture (matches mint.mjs convention):
  - evolve requires --confirm; without it, returns a dry-run plan
  - Ruflo caps: --generations 1..50, --children 1..20, --concurrency 1..8,
    --population 1..20, --cycles 1..100 (upstream supports more)
  - Upstream exit code 99 (safety-disqualified) propagates VERBATIM —
    not remapped — so CI can distinguish "evolution failed" from
    "evolution surfaced a safety-tripping mutation"
  - All three scripts emit {degraded: true, reason: 'metaharness-darwin-not-available'}
    and exit 0 when the optional dep is absent

Connection to ruflo's own ADR-155 (nightly self-learning security harness,
PR #2417, issue #2418): harness-security-bench is the closest reference
implementation for our Loop A reward-signal sanity check. Running it
periodically gives us the empirical floor — if Darwin Shield's champion
reaches TPR=1/FPR=0 on the seeded corpus, our loop A's gradient signal
is sound.

Tests:
  - plugins/ruflo-metaharness/scripts/test-graceful-degradation.mjs extended
    with the 3 new scripts; all 16 assertions pass (8 skills × 2 contracts:
    exit 0 AND emit "degraded": true)
  - npx tsc --noEmit clean on v3/@claude-flow/cli

Co-Authored-By: RuFlo <ruv@ruv.net>
2026-06-22 10:04:42 -04:00

175 lines
6.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
// security-bench.mjs — wrapper around `metaharness-darwin security bench`.
//
// "Darwin Shield" — upstream's own ADR-155 — evolves a champion harness
// on a 10-vuln/9-decoy corpus and measures it against four baselines
// (B0 static-only, B1 LLM single-pass, B2 fixed agent, B3 Darwin champion).
// The CHAMPION reaches TPR=1, FPR=0; the eight acceptance gates verify
// reproducibility, statistical significance, and unsafe-output=0.
//
// CONNECTION TO RUFLO ADR-155
// ===========================
// ruflo's ADR-155 (#2417) proposes a nightly self-learning security harness
// with three learning loops (per-dimension confidence weighting, severity
// calibration, auto-fix bid). The upstream Darwin Shield is the closest
// reference implementation — same shape, different scope (evolves a
// security-detection harness vs evaluates findings; both grade by realized
// TPR/FPR vs ground-truth corpus). Running `security bench` periodically
// gives us the empirical baseline that ruflo's Phase 2 loop A needs before
// training: if Darwin Shield converges on a known-good corpus, the loop A
// gradient signal is sound; if it doesn't, the corpus / sandbox is the
// gap, not the learning algorithm.
//
// USAGE
// node scripts/security-bench.mjs # default population=2 cycles=1
// node scripts/security-bench.mjs --population 4 --cycles 3 # deeper run
// node scripts/security-bench.mjs --population 4 --cycles 3 --alert-on-fail
//
// EXIT CODES
// 0 bench passed all gates (or degraded — Darwin not available)
// 1 --alert-on-fail and any acceptance gate failed
// 2 config error or bench infrastructure failure
import { runDarwinAsync, emitDarwinDegradedJsonAndExit } from './_darwin.mjs';
const ARGS = (() => {
const a = {
population: 2,
cycles: 1,
seed: null,
alertOnFail: false,
format: 'json',
timeoutMs: null,
};
for (let i = 2; i < process.argv.length; i++) {
const v = process.argv[i];
if (v === '--population') a.population = parseInt(process.argv[++i], 10);
else if (v === '--cycles') a.cycles = parseInt(process.argv[++i], 10);
else if (v === '--seed') a.seed = parseInt(process.argv[++i], 10);
else if (v === '--alert-on-fail') a.alertOnFail = true;
else if (v === '--format') a.format = process.argv[++i];
else if (v === '--timeout-ms') a.timeoutMs = parseInt(process.argv[++i], 10);
}
return a;
})();
function safetyChecks() {
if (ARGS.population < 1 || ARGS.population > 20) {
console.error('security-bench: --population must be 1..20 (ruflo cap)');
process.exit(2);
}
if (ARGS.cycles < 1 || ARGS.cycles > 100) {
console.error('security-bench: --cycles must be 1..100 (ruflo cap)');
process.exit(2);
}
}
function defaultTimeoutMs() {
// Bench corpus has 10 vulns + 9 decoys = 19 evaluations per cycle.
// Each evaluation runs the candidate detector + scores patches.
// Rough budget: ~3s per evaluation × population × cycles + 30s overhead.
return Math.max(60_000, 3_000 * 19 * ARGS.population * ARGS.cycles + 30_000);
}
// Parse the markdown report that `security bench` emits. The header
// "Overall: ✅ PASS" or "❌ FAIL" is the rolled-up gate. We also extract
// each acceptance gate's pass/fail line.
function parseSecurityBenchMarkdown(stdout) {
const overallMatch = /\*\*Overall:\s*([✅❌])\s*(PASS|FAIL)\*\*/i.exec(stdout);
const overall = overallMatch ? { ok: overallMatch[2].toUpperCase() === 'PASS', icon: overallMatch[1] } : null;
const gates = [];
// Match lines like: "- ✅ **TPR improvement ≥ 25% vs fixed harness** — +150%"
const gateRx = /^-\s+([✅❌])\s+\*\*(.+?)\*\*\s*[—-]\s*(.+)$/gm;
let m;
while ((m = gateRx.exec(stdout)) !== null) {
gates.push({
ok: m[1] === '✅',
criterion: m[2].trim(),
measured: m[3].trim(),
});
}
// Extract the baselines-vs-champion table — useful for diff over time.
const baselines = [];
const tableRx = /^\|\s*B[0-3]\s+([^|]+?)\s*\|\s*([\d.]+)\s*\|\s*([\d.]+)\s*\|\s*([\d.]+)\s*\|\s*([\d.]+)\s*\|\s*([\d.]+)\s*\|\s*(\d+)\s*\|\s*(.+?)\s*\|/gm;
while ((m = tableRx.exec(stdout)) !== null) {
baselines.push({
harness: m[1].trim(),
fitness: parseFloat(m[2]),
tpr: parseFloat(m[3]),
fpr: parseFloat(m[4]),
patchPass: parseFloat(m[5]),
repro: parseFloat(m[6]),
unsafe: parseInt(m[7], 10),
cost: m[8].trim(),
});
}
return { overall, gates, baselines };
}
async function main() {
safetyChecks();
const cliArgs = ['security', 'bench',
'--population', String(ARGS.population),
'--cycles', String(ARGS.cycles),
];
if (ARGS.seed != null) cliArgs.push('--seed', String(ARGS.seed));
const r = await runDarwinAsync(cliArgs, {
timeoutMs: ARGS.timeoutMs ?? defaultTimeoutMs(),
json: false, // bench output is markdown, not JSON
onProgress: (line) => { if (line.trim()) process.stderr.write(`[security-bench] ${line}\n`); },
});
if (r.degraded) {
emitDarwinDegradedJsonAndExit(r.reason);
return;
}
if (r.exitCode !== 0 && r.exitCode !== 1) {
// Exit 1 may be the bench's own "gates failed" signal; treat as data.
// Anything else is infrastructure failure.
const payload = {
success: false,
data: { exitCode: r.exitCode, stderrTail: r.stderr.slice(-400) },
generatedAt: new Date().toISOString(),
};
console.log(JSON.stringify(payload, null, 2));
process.exit(2);
}
const parsed = parseSecurityBenchMarkdown(r.stdout);
const gatesPassed = parsed.gates.filter((g) => g.ok).length;
const gatesFailed = parsed.gates.length - gatesPassed;
const payload = {
success: true,
data: {
overall: parsed.overall,
gates: {
total: parsed.gates.length,
passed: gatesPassed,
failed: gatesFailed,
details: parsed.gates,
},
baselines: parsed.baselines,
rawMarkdown: r.stdout,
shape: { population: ARGS.population, cycles: ARGS.cycles, seed: ARGS.seed },
durationMs: r.durationMs,
},
generatedAt: new Date().toISOString(),
};
console.log(JSON.stringify(payload, null, 2));
if (ARGS.alertOnFail && parsed.overall && !parsed.overall.ok) process.exit(1);
process.exit(0);
}
main().catch((e) => {
console.error(`security-bench: unexpected failure: ${e?.message ?? e}`);
process.exit(2);
});