23f7624596
ADR-166 MCP Bridge Security Lock / Static-source security lock (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / Compose default binds loopback + Mongo has auth (push) Failing after 2s
CodeQL Advanced / Analyze (rust) (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / plugin-agent-federation bindHost default (push) Failing after 1s
ADR-166 MCP Bridge Security Lock / Runtime behavior — 401 + terminal gate + fail-closed (push) Failing after 4s
business-pods-smoke / smoke (push) Failing after 1s
all-plugins-smoke / smoke-all (push) Failing after 2s
CI/CD Pipeline / Security & Code Quality (push) Failing after 1s
CI/CD Pipeline / Test Suite (ubuntu-latest) (push) Failing after 1s
CI/CD Pipeline / Build & Package (macos-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (ubuntu-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (windows-latest) (push) Has been skipped
CI/CD Pipeline / Documentation & Examples (push) Failing after 1s
Clone Tracker (14-day rolling) / Snapshot clones for ruflo ecosystem (push) Failing after 1s
CodeQL Advanced / Analyze (actions) (push) Failing after 1s
CodeQL Advanced / Analyze (javascript-typescript) (push) Failing after 1s
federation-peer-rust / stable-noop (push) Failing after 1s
metaharness-ci / score (push) Failing after 1s
metaharness-ci / router-compat (push) Failing after 0s
metaharness-ci / similarity-tests (push) Failing after 0s
no-agentbbs-smoke / smoke-without-agentbbs (push) Failing after 1s
V3 CI/CD Pipeline / Build V3 (windows-latest) (push) Has been skipped
codex-integration-audit / Codex integration audit (push) Failing after 1s
helpers-manifest-guard / guard (push) Failing after 1s
🔗 Cross-Agent Integration Tests / 🤝 Agent Coordination Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🧠 Memory Sharing Integration (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🛡️ Fault Tolerance Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / ⚡ Performance Integration Tests (push) Has been skipped
metaharness-ci / mcp-scan (push) Failing after 1s
metaharness-ci / eject-dryrun (push) Failing after 1s
metaharness-ci / metaharness-real-data (push) Failing after 0s
no-cli-optdep-bloat-2561 / guard (push) Failing after 1s
no-metaharness-smoke / smoke-without-metaharness (push) Failing after 1s
no-phantom-agentic-flow-subpath / guard (push) Failing after 1s
🔄 Automated Rollback Manager / 🚨 Failure Detection (push) Failing after 1s
V3 CI/CD Pipeline / Plugin hooks smoke / ubuntu-latest / Node 22 (push) Failing after 1s
V3 CI/CD Pipeline / ruflo-graph-intelligence build + test smoke (#2044, ADR-123) (push) Failing after 1s
CVE Audit Gate / Audit root (critical-blocking) (push) Failing after 2s
cost-tracker-smoke / smoke (push) Failing after 3s
oia-audit-weekly / audit (push) Failing after 2s
ruflo-agent-smoke / ruflo-agent structural smoke (push) Failing after 1s
📊 Status Badges Update / 📊 Update Status Badges (push) Failing after 1s
V3 CI/CD Pipeline / Static regression guards (#2267 YAML + (push) Failing after 1s
V3 CI/CD Pipeline / Test V3 Packages (push) Failing after 0s
V3 CI/CD Pipeline / agent_execute provider routing smoke (#2042) (push) Failing after 0s
CVE Audit Gate / Audit v3 (critical-blocking) (push) Failing after 1s
federation-peer-rust / stable-native (push) Failing after 2s
🔗 Cross-Agent Integration Tests / 🚀 Integration Test Setup (push) Failing after 2s
neural-trader-smoke / runtime-smoke (push) Failing after 1s
V3 CI/CD Pipeline / Build V3 (macos-latest) (push) Has been skipped
V3 CI/CD Pipeline / Build V3 (ubuntu-latest) (push) Has been skipped
V3 CI/CD Pipeline / Type Check V3 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 24 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 22 (push) Failing after 2s
V3 CI/CD Pipeline / browser rvf create flag smoke (#2015) (push) Failing after 0s
V3 CI/CD Pipeline / Dependency review (#2046) (push) Has been skipped
V3 CI/CD Pipeline / Supply-chain audit (#2046) (push) Failing after 0s
V3 CI/CD Pipeline / witness marker drift smoke (#2021) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader portfolio CG smoke (#2068, ADR-126 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader backtest signing smoke (#2068, ADR-126 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / kg-extract type-import classification smoke (#2049) (push) Failing after 0s
V3 CI/CD Pipeline / witness verify precondition smoke (#1880) (push) Failing after 2s
V3 CI/CD Pipeline / neural-trader pipeline risk-gate smoke (#2068, ADR-126 Phase 5) (push) Failing after 0s
V3 CI/CD Pipeline / neural-trader feature attribution smoke (#2068, ADR-126 Phase 6) (push) Failing after 0s
V3 CI/CD Pipeline / plugin-registry signature verification smoke (#1922, CWE-347) (push) Failing after 4s
V3 CI/CD Pipeline / memory stats legacy-DB smoke (#2120) (push) Failing after 4s
V3 CI/CD Pipeline / github deprecated actions smoke (#2089, ADR-127 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / graph query + pathfinder smoke (ADR-130 P2+P5) (push) Has been skipped
V3 CI/CD Pipeline / graph trajectory hooks smoke (ADR-130 P3) (push) Has been skipped
V3 CI/CD Pipeline / graph plugin adapter smoke (ADR-130 P4) (push) Has been skipped
V3 CI/CD Pipeline / graph benchmark (ADR-130 P6) (push) Has been skipped
V3 CI/CD Pipeline / statusline generator delegation smoke (#2195) (push) Failing after 1s
V3 CI/CD Pipeline / wizard init regression guard (#2206 (push) Failing after 1s
V3 CI/CD Pipeline / memory no-stray-db smoke (ADR-125 P7) (push) Failing after 1s
V3 CI/CD Pipeline / github-safe injection smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github actions pin smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github attribution opt-in smoke (#2089, ADR-127 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / pre-bash hook safety smoke (#2017) (push) Failing after 1s
V3 CI/CD Pipeline / Memory import smoke / ubuntu-latest (push) Failing after 0s
V3 CI/CD Pipeline / MCP protocol smoke / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / ruvllm WASM auto-init smoke (#2086) (push) Failing after 4s
V3 CI/CD Pipeline / MCP paired-tool round-trip smoke (#1889) (push) Failing after 1s
V3 CI/CD Pipeline / Plugin package install-safety (#1902/#1903/#1904) (push) Failing after 1s
V3 CI/CD Pipeline / Tool description discoverability (ADR-112) (push) Failing after 3s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (22) (push) Failing after 1s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (24) (push) Failing after 1s
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Vector-index dimension audit (#1947) (push) Failing after 0s
V3 CI/CD Pipeline / Hook-command install safety (#1921) (push) Failing after 1s
V3 CI/CD Pipeline / ToolOutputGuardrail smoke (ADR-131, (push) Failing after 1s
V3 CI/CD Pipeline / init-bundle invariants smoke (#2095, ADR-128 Phase 5) (push) Failing after 1s
V3 CI/CD Pipeline / wasm provider bridge smoke (ADR-129 P1) (push) Failing after 2s
V3 CI/CD Pipeline / wasm gallery CRUD smoke (ADR-129 P3) (push) Failing after 1s
V3 CI/CD Pipeline / wasm plugin bridge smoke (ADR-129 P4) (push) Failing after 0s
V3 CI/CD Pipeline / wasm compose smoke (ADR-129 P2) (push) Failing after 4s
V3 CI/CD Pipeline / graph schema smoke (ADR-130 P1) (push) Failing after 0s
Validate Marketplace / validate (push) Failing after 1s
🔍 Verification Pipeline / 🚀 Setup Verification (push) Failing after 1s
🔍 Verification Pipeline / 🛡️ Security Verification (push) Has been skipped
🔍 Verification Pipeline / 📝 Code Quality (push) Has been skipped
🔍 Verification Pipeline / 🧪 Test Verification (${{ matrix.os }}, Node ${{ matrix.node }}) (push) Has been skipped
🔍 Verification Pipeline / 🏗️ Build Verification (push) Has been skipped
🔍 Verification Pipeline / 📚 Documentation Verification (push) Has been skipped
CVE Audit Gate / High-severity report (warn only) (push) Has been cancelled
🔄 Automated Rollback Manager / 🔄 Execute Rollback (push) Has been cancelled
🔄 Automated Rollback Manager / ✅ Post-Rollback Verification (push) Has been cancelled
🔄 Automated Rollback Manager / 📊 Rollback Monitoring (push) Has been cancelled
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / windows-latest (push) Has been cancelled
🔄 Automated Rollback Manager / ⏳ Manual Rollback Approval (push) Has been cancelled
V3 CI/CD Pipeline / MCP protocol smoke / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Memory import smoke / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / ubuntu-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Publish to npm (alpha) (push) Has been cancelled
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / macos-latest / Node 22 (push) Has been cancelled
V3 CI/CD Pipeline / Plugin hooks smoke / macos-latest / Node 22 (push) Has been cancelled
CI/CD Pipeline / Deploy & Release (push) Has been cancelled
CI/CD Pipeline / CI Status (push) Has been cancelled
🔗 Cross-Agent Integration Tests / 📊 Integration Test Report (push) Has been cancelled
🔄 Automated Rollback Manager / 🔍 Pre-Rollback Validation (push) Has been cancelled
🔍 Verification Pipeline / ⚡ Performance Verification (push) Has been cancelled
🔍 Verification Pipeline / 📊 Verification Report (push) Has been cancelled
331 lines
12 KiB
JavaScript
Executable File
331 lines
12 KiB
JavaScript
Executable File
// _harness.mjs — shared invocation helper for the metaharness/harness CLIs.
|
||
//
|
||
// All ruflo-metaharness skills shell out to the upstream CLI rather than
|
||
// linking the library — this honors ADR-150's architectural constraint
|
||
// (MetaHarness must remain a removable augmentation, never a required
|
||
// runtime dep) while still giving us "deep integration" through a single
|
||
// vetted bridge that every skill imports from.
|
||
//
|
||
// CONTRACT
|
||
// - `runMetaharness(args, opts)` — invoke the `metaharness` binary
|
||
// - `runHarness(args, opts)` — invoke the sibling `harness` binary
|
||
// - both return `{ stdout, stderr, exitCode, json|null, durationMs, degraded, reason? }`
|
||
// - `--json` flag is appended automatically when `opts.json !== false`
|
||
// - subprocess hard timeout (default 60s) — captured in opts.timeoutMs
|
||
// - when the package cannot be resolved/installed, returns degraded result
|
||
// with `degraded: true, reason: 'metaharness-not-available'` — never
|
||
// throws (ADR-150 graceful-degradation rule #3). A subprocess killed by
|
||
// the timeout reports `reason: 'metaharness-timeout'` instead (matches
|
||
// the sibling _darwin/_redblue helpers).
|
||
//
|
||
// SECURITY + PERF (supersedes the iter-27 npx-with-@latest-dist-tag path)
|
||
// =========================================================================
|
||
// The pre-consolidation implementation shelled to `npx -y` with the @latest
|
||
// dist-tag. Two problems, both fixed here:
|
||
// 1. SECURITY (HIGH): @latest means a compromised upstream publish executes
|
||
// arbitrary code on user machines on the very next skill invocation.
|
||
// The version is now PINNED to METAHARNESS_PIN_VERSION (tilde range —
|
||
// patch updates only) and only bumped deliberately, in lock-step with
|
||
// optionalDependencies in @claude-flow/cli + ruflo package.json.
|
||
// 2. PERF: @latest forced an npm-registry metadata check on EVERY call.
|
||
// Resolution is now (a) an already-installed local metaharness
|
||
// satisfying the pin (walk-up node_modules — free), then (b) a ONE-TIME
|
||
// `npm install --prefix ~/.ruflo/metaharness-cache-<pin>` versioned
|
||
// cache (same proven pattern as _redblue.mjs / gepa.mjs), after which
|
||
// every call is a plain `node <abs-path-to-cli>` spawn — zero network.
|
||
//
|
||
// BOTH BINARIES: the `metaharness` package ships two bins (`metaharness`
|
||
// and `harness`); their entry paths are read from the resolved package.json
|
||
// bin map rather than hardcoded, so upstream layout changes inside the
|
||
// pinned range cannot silently break us.
|
||
|
||
import { spawnSync, spawn } from 'node:child_process';
|
||
import { existsSync, readFileSync } from 'node:fs';
|
||
import { join } from 'node:path';
|
||
import {
|
||
classifyDegraded,
|
||
ensureCachedInstall,
|
||
findLocalPackageDir,
|
||
injectJson,
|
||
makeDegradedEmitter,
|
||
parseTrailingJson,
|
||
} from './_invoke.mjs';
|
||
|
||
const DEFAULT_TIMEOUT_MS = 60_000;
|
||
|
||
// Pinned semver range. Bump in lock-step with optionalDependencies in
|
||
// @claude-flow/cli/package.json + ruflo/package.json. NEVER @latest.
|
||
const METAHARNESS_PKG = 'metaharness';
|
||
const METAHARNESS_PIN_VERSION = '~0.3.0';
|
||
|
||
const REASON_NOT_AVAILABLE = 'metaharness-not-available';
|
||
|
||
/**
|
||
* Resolve absolute paths for the two bins shipped by the metaharness
|
||
* package. Memoized per process — the whole point is that after the first
|
||
* resolution (or one-time cache install) every call is local-only.
|
||
*/
|
||
let RESOLVED = null;
|
||
function resolveMetaharnessBins() {
|
||
if (RESOLVED) return RESOLVED;
|
||
// (a) already-installed local copy satisfying the pin — free.
|
||
const localDir = findLocalPackageDir(METAHARNESS_PKG, METAHARNESS_PIN_VERSION);
|
||
if (localDir) {
|
||
const bins = readBinMap(localDir);
|
||
if (bins) return (RESOLVED = { ok: true, bins, source: 'local' });
|
||
}
|
||
// (b) one-time versioned cache install of the pinned range.
|
||
const cached = ensureCachedInstall({ pkg: METAHARNESS_PKG, pinVersion: METAHARNESS_PIN_VERSION });
|
||
if (!cached.ok) {
|
||
return (RESOLVED = {
|
||
ok: false,
|
||
reason: REASON_NOT_AVAILABLE,
|
||
stderr: cached.stderr ?? cached.error ?? '',
|
||
stdout: cached.stdout ?? '',
|
||
});
|
||
}
|
||
const bins = readBinMap(cached.pkgDir);
|
||
if (!bins) {
|
||
return (RESOLVED = {
|
||
ok: false,
|
||
reason: REASON_NOT_AVAILABLE,
|
||
stderr: `metaharness package at ${cached.pkgDir} is missing the expected bin map (metaharness + harness)`,
|
||
stdout: '',
|
||
});
|
||
}
|
||
return (RESOLVED = { ok: true, bins, source: 'cache' });
|
||
}
|
||
|
||
/** Read both bin entry points from the package's bin map. */
|
||
function readBinMap(pkgDir) {
|
||
try {
|
||
const pj = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf-8'));
|
||
const bin = pj.bin || {};
|
||
if (!bin.metaharness || !bin.harness) return null;
|
||
const metaharness = join(pkgDir, bin.metaharness);
|
||
const harness = join(pkgDir, bin.harness);
|
||
if (!existsSync(metaharness) || !existsSync(harness)) return null;
|
||
return { metaharness, harness };
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function degradedResult(resolved, start) {
|
||
return {
|
||
stdout: resolved.stdout ?? '',
|
||
stderr: resolved.stderr ?? '',
|
||
exitCode: 127,
|
||
json: null,
|
||
durationMs: Date.now() - start,
|
||
degraded: true,
|
||
reason: resolved.reason ?? REASON_NOT_AVAILABLE,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Async variant (iter 56) — used by oia-audit.mjs to parallelize its 5
|
||
* subprocess calls, dropping worst-case wall-clock from 5×TIMEOUT to
|
||
* 1×TIMEOUT. Identical return shape to the sync path.
|
||
*/
|
||
function execBinAsync(binName, args, opts = {}) {
|
||
const start = Date.now();
|
||
const resolved = resolveMetaharnessBins();
|
||
if (!resolved.ok) return Promise.resolve(degradedResult(resolved, start));
|
||
return new Promise((resolve) => {
|
||
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
||
const wantJson = opts.json !== false;
|
||
const argv = injectJson(args, wantJson);
|
||
const p = spawn('node', [resolved.bins[binName], ...argv], {
|
||
stdio: ['ignore', 'pipe', 'pipe'],
|
||
cwd: opts.cwd,
|
||
env: { ...process.env, ...(opts.env || {}) },
|
||
shell: false,
|
||
});
|
||
let stdout = '', stderr = '';
|
||
p.stdout?.on('data', (d) => { stdout += d.toString(); });
|
||
p.stderr?.on('data', (d) => { stderr += d.toString(); });
|
||
let timedOut = false;
|
||
const timer = setTimeout(() => {
|
||
timedOut = true;
|
||
try { p.kill('SIGTERM'); } catch { /* ignore */ }
|
||
}, timeoutMs);
|
||
p.on('error', (e) => {
|
||
clearTimeout(timer);
|
||
resolve({
|
||
stdout, stderr: stderr + String(e?.message ?? e),
|
||
exitCode: 127, json: null, durationMs: Date.now() - start,
|
||
degraded: true, reason: REASON_NOT_AVAILABLE,
|
||
});
|
||
});
|
||
p.on('close', (code) => {
|
||
clearTimeout(timer);
|
||
const durationMs = Date.now() - start;
|
||
const classified = classifyDegraded(stderr, timedOut ? null : code, 'metaharness');
|
||
if (classified.degraded) {
|
||
resolve({
|
||
stdout, stderr,
|
||
exitCode: code ?? 127, json: null, durationMs,
|
||
degraded: true, reason: classified.reason,
|
||
});
|
||
return;
|
||
}
|
||
resolve({
|
||
stdout, stderr,
|
||
exitCode: code ?? 0,
|
||
json: wantJson ? parseTrailingJson(stdout) : null,
|
||
durationMs,
|
||
degraded: false,
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
/** Sync invocation of one of the two resolved bins via `node <abs-path>`. */
|
||
function execBin(binName, args, opts = {}) {
|
||
const start = Date.now();
|
||
const resolved = resolveMetaharnessBins();
|
||
if (!resolved.ok) return degradedResult(resolved, start);
|
||
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
||
const wantJson = opts.json !== false;
|
||
const argv = injectJson(args, wantJson);
|
||
const r = spawnSync('node', [resolved.bins[binName], ...argv], {
|
||
stdio: ['ignore', 'pipe', 'pipe'],
|
||
encoding: 'utf-8',
|
||
timeout: timeoutMs,
|
||
cwd: opts.cwd, // iter 27 — let callers redirect $CWD (mint.mjs needs this)
|
||
env: { ...process.env, ...(opts.env || {}) },
|
||
shell: false,
|
||
});
|
||
const durationMs = Date.now() - start;
|
||
const stdout = r.stdout || '';
|
||
const stderr = r.stderr || '';
|
||
const classified = classifyDegraded(stderr, r.status, 'metaharness');
|
||
if (classified.degraded) {
|
||
return {
|
||
stdout, stderr,
|
||
exitCode: r.status ?? 127,
|
||
json: null,
|
||
durationMs,
|
||
degraded: true,
|
||
reason: classified.reason,
|
||
};
|
||
}
|
||
return {
|
||
stdout, stderr,
|
||
exitCode: r.status ?? 0,
|
||
json: wantJson ? parseTrailingJson(stdout) : null,
|
||
durationMs,
|
||
degraded: false,
|
||
};
|
||
}
|
||
|
||
export function runMetaharness(args, opts) {
|
||
return execBin('metaharness', args, opts);
|
||
}
|
||
|
||
export function runHarness(args, opts) {
|
||
// The `harness` binary ships inside the same `metaharness` package —
|
||
// resolved from the package.json bin map above.
|
||
return execBin('harness', args, opts);
|
||
}
|
||
|
||
export function runMetaharnessAsync(args, opts) {
|
||
return execBinAsync('metaharness', args, opts);
|
||
}
|
||
|
||
export function runHarnessAsync(args, opts) {
|
||
return execBinAsync('harness', args, opts);
|
||
}
|
||
|
||
/**
|
||
* iter 63 — single source of truth for severity ranks across the
|
||
* metaharness plugin family. Pre-iter-63, three scripts (oia-audit,
|
||
* audit-trend, mcp-scan) maintained their own SEVERITY_RANK literal,
|
||
* each missing different keys, each producing different NaN-compare
|
||
* behavior on unknown severities. Iter 62 fixed oia-audit; iter 63
|
||
* propagates the fix and consolidates.
|
||
*
|
||
* Mapping rationale:
|
||
* clean / info → 0 (no harm)
|
||
* low → 1
|
||
* medium / warn → 2
|
||
* high / error → 3
|
||
* critical → 4 (explicit elevation above high)
|
||
*
|
||
* `rankSeverity(s)` is the safe accessor — returns 0 for any unknown
|
||
* string instead of `undefined`, eliminating the NaN-compare hazard
|
||
* (`undefined > 3` evaluates to false → unknown severities silently
|
||
* ignored in reduce expressions).
|
||
*/
|
||
export const SEVERITY_RANK = Object.freeze({
|
||
clean: 0, info: 0,
|
||
low: 1,
|
||
medium: 2, warn: 2,
|
||
high: 3, error: 3,
|
||
critical: 4,
|
||
});
|
||
|
||
export function rankSeverity(s) {
|
||
if (s == null) return 0;
|
||
return SEVERITY_RANK[String(s).toLowerCase()] ?? 0;
|
||
}
|
||
|
||
/**
|
||
* iter 50 — parse `harness mcp-scan` text output into structured findings.
|
||
*
|
||
* Upstream `harness mcp-scan` emits plain text even with --json:
|
||
*
|
||
* harness mcp-scan — <path>
|
||
*
|
||
* [INFO] No MCP security issues found
|
||
* Policy is default-deny with safe capability grants and an audit log.
|
||
*
|
||
* Result: INFO (1 finding, 0 high)
|
||
*
|
||
* Closes the iter-49-flagged gap where audit-trend.mjs reads
|
||
* `json.findings` expecting an array, but mcp-scan's r.json was null.
|
||
* Used by BOTH mcp-scan.mjs (the wrapper) and oia-audit.mjs (composite
|
||
* audit) so the structured-findings invariant holds across the pipeline.
|
||
*/
|
||
export function parseMcpScanText(stdout) {
|
||
const findings = [];
|
||
const lines = (stdout || '').split('\n');
|
||
let current = null;
|
||
for (const line of lines) {
|
||
const m = /^\s*\[([A-Z]+)\]\s+(.+?)\s*$/.exec(line);
|
||
if (m) {
|
||
if (current) findings.push(current);
|
||
current = { severity: m[1].toLowerCase(), message: m[2] };
|
||
} else if (current && /^\s{6,}\S/.test(line)) {
|
||
const cont = line.trim();
|
||
if (cont) current.message += ' ' + cont;
|
||
} else if (current && line.trim() === '') {
|
||
findings.push(current);
|
||
current = null;
|
||
}
|
||
}
|
||
if (current) findings.push(current);
|
||
const resultMatch = /Result:\s+([A-Z]+)\s+\((\d+)\s+finding/i.exec(stdout);
|
||
const summary = resultMatch ? {
|
||
overallSeverity: resultMatch[1].toLowerCase(),
|
||
totalCount: parseInt(resultMatch[2], 10),
|
||
} : null;
|
||
return { findings, summary };
|
||
}
|
||
|
||
// Convenience emitter for skill scripts — keep the boilerplate out of
|
||
// each skill so they focus on argument parsing + exit-code semantics.
|
||
// Exit 0 — ADR-150 architectural constraint says ruflo continues to
|
||
// function when MetaHarness is absent. Skills emit a structured
|
||
// degraded payload rather than failing.
|
||
export const emitDegradedJsonAndExit = makeDegradedEmitter(METAHARNESS_PKG, METAHARNESS_PIN_VERSION);
|
||
|
||
export const METAHARNESS_VERSION_PIN = `${METAHARNESS_PKG}@${METAHARNESS_PIN_VERSION}`;
|
||
|
||
/** Expose resolution for diagnostics/tests (which path served the bins). */
|
||
export function metaharnessResolution() {
|
||
const r = resolveMetaharnessBins();
|
||
return r.ok ? { ok: true, source: r.source, bins: r.bins } : { ok: false, reason: r.reason };
|
||
}
|