* fix(security): ADR-165 Phase 1 — close all critical CVEs, refresh registry, add CI gate Closes ADR-165 Phase 1. Both workspaces pass `npm audit --audit-level=critical`. ## Before (2026-06-29) | Workspace | Critical | High | Moderate | Total | |-----------|----------|------|----------|-------| | Root | **1** | 6 | 31 | 38 | | v3 | **4** | 33 | 57 | 97 | ## After | Workspace | Critical | High | Moderate | Total | |-----------|----------|------|----------|-------| | Root | **0** | 0 | 31 | 31 | | v3 | **0** | 27 | 58 | 88 | All 5 critical advisories closed (vitest GHSA-5xrq, handlebars prototype-pollution, protobufjs constructor-pollution, plus 2 more in v3). 12 of 39 high advisories closed via overrides; the remaining 27 v3 highs are deeper transitive chains that would require breaking-change major bumps — flagged for ADR-165 Phase 2. ## Changes ### Root workspace overrides (package.json) - `vitest`: `^1.0.0` → `^3.2.6` (closes critical GHSA-5xrq CVSS 9.8) - `hono`: `>=4.11.4` → `>=4.12.25` (closes 6 hono advisories) - `undici`: `>=7.18.0` → `>=8.5.0` (closes 7 undici advisories — bumps the stale pin that ADR-165 flagged as still-in-vulnerable-range) - `vite`: `>=6.4.6` → `>=8.0.16` (closes 2 vite advisories) - NEW `@grpc/grpc-js`: `>=1.14.4` - NEW `form-data`: `>=4.0.6` - NEW `http-proxy-middleware`: `>=3.0.7` - `@hono/node-server`: `>=1.19.10` → `>=1.19.14` ### v3 workspace overrides (v3/package.json + pnpm.overrides) - `vitest`: `^4.0.16` → `^4.1.0` (and `@vitest/coverage-v8` matched) - Top-level + pnpm overrides: `handlebars: >=4.7.9`, `protobufjs: >=8.6.0` - 27 sub-package `package.json` updated to `vitest ^4.1.0` ### CVE registry refresh (v3/@claude-flow/security/src/CVE-REMEDIATION.ts) Rewritten from 5 stale Jan-2026 entries to 16 total: 15 fixed, 1 open (ADR165-OPEN-01 — PII pipeline wiring per ADR-164 §6.1 not yet on dispatch-layer). `SECURITY_SUMMARY` is now computed dynamically from the registry; `validateRemediation()` correctly returns `allFixed: false` with 1 issue. ADR-165 §5 flagged this as THE Phase 2 priority — now closed. ### New CI gate `.github/workflows/cve-audit.yml`: - `audit-root` — `npm audit --audit-level=critical` in root, blocking - `audit-v3` — same in v3, blocking - `audit-high-report` — warn-only summary on high-severity drift - Triggers: PR, push to main, daily cron ### ADR-165 §9 evidence ledger addendum Captures AFTER-remediation `npm audit --json` metadata for both workspaces. The diff between BEFORE (already in §9) and AFTER makes Phase 1 verifiable. Also CORRECTED one prior surprise in §9: the `validate-input.ts` in `mcp-tools/` is NOT a 9-line shim. It's 269 lines of real implementation. (That was an inaccurate findings note from the initial audit.) ### §7.3 research items resolved - `protobufjs` v3 chain confirmed gone after override pins - `handlebars` v3 chain confirmed not user-reachable on the workflow path (toolchain-only; override sufficient) - `validate-input.ts` confirmed real implementation, not a shim (registry entry corrected) - CORS surface check: no `cors()` / `app.use(cors` call sites — the hono CORS-wildcard advisory is not exploitable on the current server - ADR-164 §6.1 PII pipeline NOT wired in current agentbbs dispatch — recorded as ADR165-OPEN-01 (Phase 3 work) ## Gates (all PASS) - `npm audit --audit-level=critical` root: exit 0 (was non-zero) - `npm audit --audit-level=critical` v3: exit 0 (was non-zero) - vitest suites: 105/109 pass (4 skip-conditional — same as before) - ADR-112 audit: 352/352 tools with guidance - business-pods smoke: 11/11 PASS - agentbbs smoke: 8/8 PASS - agenticow smoke: 8/8 PASS - cli build: clean (tsc) ## Phase 2+ deferred The 27 v3 high-severity advisories that remain are transitive chains with no patched-in-range version. ADR-165 §6 Phase 2 picks these up after the `cve-watch.yml` automation in §6 lands (NEW PR — not in scope here). Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01WbBa4nccx5aGhXphoHxcni * chore(lock): regen v3/pnpm-lock.yaml after override updates (PR #2508 CI fix) PR #2508 added handlebars + protobufjs to v3 overrides + bumped vitest pin, but v3/pnpm-lock.yaml wasn't regenerated. CI surfaced ERR_PNPM_LOCKFILE_CONFIG_MISMATCH on every install step. Same pattern as the previous lockfile-drift fixes on PR #2500 and #2503. * fix(ci): cve-audit.yml jq-only + regen witness manifests for #1609 marker drift Two PR #2508 CI failures fixed: 1. Static-regression-guard tripped on cve-audit.yml line 52: the static YAML strict-parser sees `if high > 0:` inside the `run: |` block as a YAML block-mapping key indicator. Refactored all 3 inline-python blocks to use jq (preinstalled on ubuntu-latest) — no embedded `if x > 0:` lines, so the YAML guard passes. 2. Witness marker drift smoke flagged fix-marker #1609 — the Phase 1 vitest bump (4.0.16 → 4.1.0) drifted the cited marker string in v3/@claude-flow/aidefence/package.json. Updated `marker` field in all 3 platform manifests + `desc` to reference ADR-165 Phase 1, then re-signed via plugins/ruflo-core/scripts/witness/regen.mjs. Co-Authored-By: RuFlo <ruv@ruv.net>
Ruflo Verification System
A cross-platform, cryptographically-signed regression-protection toolkit. Every documented fix in ruflo gets attested by a SHA-256 + marker substring + Ed25519 signature. Anyone with the same git commit can re-derive the public key and verify it independently.
Three-layer regression protection:
- Behavioral smoke tests (CI) — exercise user-visible failure modes
- Cryptographic witness manifest (this folder) — attest fix presence
- Temporal history (
<os>/history.jsonl) — bisect when a regression was introducedSee docs/validation/README.md for the full architecture, ADR-102 for the smoke-harness pattern, and ADR-103 for the temporal-history layer.
Why this exists
Three regressions filed on 2026-05-08 (#1859, #1862, #1867) all passed unit tests on the broken commits but broke for users on first install. Unit tests verify code paths; users hit subprocess flag parsers, fresh npm install resolution, plugin/CLI version drift. The witness layer attests that every documented fix's load-bearing code is still present — even when the fix has no dedicated smoke test.
A regression that deletes the load-bearing line of a fix flips its markerVerified to false. The CI's witness-verify job blocks publish.
Folder layout
verification/
README.md ← you are here
witness-fixes.json ← input config (OS-independent)
mcp-tool-baseline.json ← ADR-112 — monotone-decreasing tool-discoverability baseline
results.md ← human-readable last-run report
inventory.json ← reserved for capability inventory
linux/
manifest.md.json ← signed by Linux CI runner
history.jsonl ← Linux temporal log
macos/
manifest.md.json ← signed by macOS CI runner
history.jsonl ← macOS temporal log
windows/
manifest.md.json ← signed by Windows CI runner
history.jsonl ← Windows temporal log
Why per-OS? File hashes drift across platforms (LF vs CRLF normalization, path separators, prebuilt binary differences). Each OS produces its own truthful snapshot. The witness-verify CI job runs once per OS and writes/verifies that OS's bundle, catching platform-specific marker drift before it reaches users on that platform.
Why JSONL inside each OS folder? The witness use case is structured key-value retrieval (snapshot N's commit + fix statuses), not vector similarity search. JSONL is git-diff-friendly, append-cheap, and human-readable — the better fit. The folder layout adopts the RVF-style cognitive container concept (per-OS bundle of manifest + history + metadata); the file format inside is JSONL.
Tutorial — adopt the witness in your own project
1. Bootstrap
The witness toolkit lives in plugins/ruflo-core/scripts/witness/ — copy it into your project (or install the ruflo-core plugin).
# Initialize per-OS folders + sample fix template
node plugins/ruflo-core/scripts/witness/init.mjs --root .
# Single runtime dependency
npm i @noble/ed25519
Outputs:
verification/
witness-fixes.json # template — edit with your fixes
<os>/
manifest.md.json # empty seed
history.jsonl # empty seed
2. Register your first fix
Open verification/witness-fixes.json:
{
"fixes": [
{
"id": "MY-001",
"desc": "Race condition in token refresh — early-return on in-flight refresh",
"file": "dist/auth.js",
"marker": "if (this._refreshing) return this._refreshing;"
}
]
}
Choosing a marker is the load-bearing skill:
| Bad marker | Why it fails |
|---|---|
'function' |
False-positives in any file with a function |
'TODO' |
Flaps as TODOs come and go |
'fix' |
Too generic |
| Good marker | Why it works |
|---|---|
(await import('better-sqlite3')).default |
Distinctive AND specific to the fix mechanism |
(ctx.flags.file as string) || ctx.args[0] |
The exact swap that fixes the bug |
import * as bcrypt from 'bcryptjs' |
Proves the bcrypt → bcryptjs migration is in dist |
Pick a unique substring from the diff that's specifically created by the fix.
3. Generate the signed manifest
In ruflo's monorepo there's a wrapper that hard-codes paths:
node scripts/regen-witness.mjs
For external projects, call the plugin script with explicit paths:
node plugins/ruflo-core/scripts/witness/regen.mjs \
--manifest verification/macos/manifest.md.json \
--history verification/macos/history.jsonl \
--fixes verification/witness-fixes.json
Output:
witness regen summary
─────────────────────
gitCommit: abc123def456…
branch: main
issuedAt: 2026-05-09T00:00:00.000Z
total fixes: 1 (was 0)
verified: 1
missing: 0
new entries: MY-001
written: verification/macos/manifest.md.json
appended: verification/macos/history.jsonl
verified: N/N confirms every marker is present in its cited file.
4. Verify
node plugins/ruflo-core/scripts/witness/verify.mjs \
--manifest verification/macos/manifest.md.json
Output:
Manifest signature:
hash matches: yes
public key reproducible: yes
Ed25519 signature valid: yes
Summary: pass=1 drift=0 regressed=0 missing=0
If regressed > 0 or signatureValid: no, the manifest has been tampered with or a documented fix's marker has been deleted.
5. Investigate a regression
When CI reports F12 regressed:
node plugins/ruflo-core/scripts/witness/history.mjs \
--history verification/macos/history.jsonl regressions
Output:
F12
last pass: a1b2c3d4 2026-05-07T14:23:11.000Z
regressed at: 9f8e7d6c 2026-05-08T09:14:55.000Z
Then git log lastPass..regressedAt -- <file> collapses bisect from "many commits" to "the few in this 18-hour window."
6. Wire into CI
Add to .github/workflows/<your-pipeline>.yml:
witness-verify:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '22' }
- run: npm i @noble/ed25519
- shell: bash
run: |
case "$RUNNER_OS" in
Linux) OS_DIR=linux ;;
macOS) OS_DIR=macos ;;
Windows) OS_DIR=windows ;;
esac
node plugins/ruflo-core/scripts/witness/verify.mjs \
--manifest "verification/$OS_DIR/manifest.md.json" \
--json > /tmp/result.json
node -e "if (!require('/tmp/result.json').ok) process.exit(1)"
Block publish on failure. Every commit on every OS gets attested.
Features
| Feature | Mechanism | What it catches |
|---|---|---|
| SHA-256 fingerprint per fix | Per-file hash recorded at issuance | Any change to the file |
| Marker substring | Distinctive code substring required to be present | Refactors that delete the fix |
| Ed25519 signature | Deterministic seed sha256(gitCommit + ':ruflo-witness/v1') |
Manifest tampering, key-derivation drift |
| Reproducible public key | Anyone with the git commit re-derives the same pubkey | No committed private key needed |
| Per-OS bundles | verification/{linux,macos,windows}/ |
Platform-specific marker drift (CRLF, path separators) |
| Temporal history (JSONL) | One snapshot appended per regen | When a regression was introduced |
| Drift vs regression | sha256 changed but marker present → drift; marker missing → regression | False alarms suppressed; real regressions flagged |
| Status timeline per fix | history.mjs timeline --id F12 |
Flapping markers (too brittle) and persistent drift (too unstable) |
Daily workflow cheat sheet
# Add a new fix
$EDITOR verification/witness-fixes.json
node scripts/regen-witness.mjs
# Verify the current tree
node plugins/ruflo-core/scripts/witness/verify.mjs \
--manifest verification/macos/manifest.md.json
# Diff against the previous snapshot
node plugins/ruflo-core/scripts/witness/history.mjs \
--history verification/macos/history.jsonl summary
# Find when a regression was introduced
node plugins/ruflo-core/scripts/witness/history.mjs \
--history verification/macos/history.jsonl regressions
# Status timeline for one fix
node plugins/ruflo-core/scripts/witness/history.mjs \
--history verification/macos/history.jsonl timeline --id F12
Schema
<os>/manifest.md.json
{
"manifest": {
"schema": "ruflo-witness/v1",
"issuedAt": "2026-05-09T00:00:00.000Z",
"gitCommit": "<full sha>",
"branch": "main",
"os": "macos", // matches the folder
"releases": { "ruflo": "3.7.0-alpha.21", "@claude-flow/plugin-agent-federation": "1.0.0-alpha.15" },
"summary": { "totalFixes": 102, "verified": 102, "missing": 0 },
"fixes": [
{
"id": "F1",
"desc": "...",
"file": "v3/@claude-flow/cli/dist/...",
"sha256": "<64 hex>",
"marker": "<distinctive substring>",
"markerVerified": true
}
]
},
"integrity": {
"manifestHashAlgo": "sha256",
"manifestHash": "<64 hex of canonical manifest>",
"signature": "<64-byte hex of Ed25519 signature over canonical manifest>",
"signatureAlgo": "ed25519",
"publicKey": "<32-byte hex>",
"signature": "<64-byte hex>",
"seedDerivation": "sha256(gitCommit + ':ruflo-witness/v1')"
}
}
<os>/history.jsonl (one entry per line)
{
"v": 1,
"commit": "<full sha>",
"issuedAt": "2026-05-09T00:00:00.000Z",
"branch": "main",
"os": "macos",
"manifestHash": "<hex>",
"summary": { "totalFixes": 82, "verified": 82, "missing": 0 },
"fixes": {
"F1": { "sha256": "...", "markerVerified": true },
"#1867": { "sha256": "...", "markerVerified": true }
}
}
witness-fixes.json (input, OS-independent)
{
"fixes": [
{
"id": "MY-001",
"desc": "human-readable description",
"file": "path/relative/to/repo/root.js",
"marker": "distinctive substring that must remain in the file"
}
]
}
Anti-patterns
- Hand-editing
manifest.md.json— breaks the signature. Always regenerate viaregen.mjs. - Markers that match generic patterns (
'function','import','TODO') — false positives or flapping. - Skipping the history append — without
--history, you lose Layer 3 (regression introduction time). - Committing the manifest without the history — they belong in the same commit.
- Cross-OS manifest editing — the manifest is signed for the OS that generated it. Don't copy a Linux manifest into the macOS folder.
How it integrates with the rest of ruflo's regression stack
| Layer | Where | What it catches |
|---|---|---|
| Layer 1 — install smoke | v3/@claude-flow/memory/scripts/smoke-no-bsqlite.mjs + CI smoke-install-no-bsqlite |
npm install failures on platforms without prebuilds |
| Layer 1 — hook smoke | plugins/ruflo-core/scripts/test-hooks.mjs + CI plugin-hooks-smoke |
Plugin/CLI flag drift, parser ambiguity |
| Layer 1 — MCP protocol smoke | plugins/ruflo-core/scripts/test-mcp-protocol.mjs + CI mcp-protocol-smoke |
HTTP MCP wire-format compliance |
| Layer 1 — memory-import smoke | plugins/ruflo-core/scripts/test-memory-import.mjs + CI memory-import-smoke |
Memory_import_claude WSL path + key sanitization regressions |
| Layer 1 — tool-description audit | scripts/audit-tool-descriptions.mjs + CI tool-descriptions-audit (ADR-112) |
Every MCP tool description must have "Use when … is wrong because …" guidance + length ≥ 80 + unique. Baseline at verification/mcp-tool-baseline.json monotone-decreasing |
| Layer 2 — witness manifest | verification/<os>/manifest.md.json + CI witness-verify |
Documented fix marker disappearing |
| Layer 3 — temporal history | verification/<os>/history.jsonl + history.mjs |
When a regression was introduced |
All CI jobs gate publish. A regression in any layer blocks the release on the platform where it was caught.
References
- docs/validation/README.md — full architecture overview (in-repo)
- Public gist — external-friendly version
- ADR-102 — smoke harness pattern + flag-priority CLI convention
- ADR-103 — JSONL history layer + plugin distribution
plugins/ruflo-core/skills/witness/SKILL.md— Claude Code skill workflowplugins/ruflo-core/agents/witness-curator.md— curator agent definition.github/workflows/v3-ci.yml—smoke-install-no-bsqlite,plugin-hooks-smoke,witness-verifyjobs