* fix(security): compare candidate and prefix in the same canonical form (#3010)
`validate()` resolved the candidate with `fs.realpath` but the allowed
prefixes were only `path.resolve()`d, so the two sides were compared in
different forms and nothing under a prefix reached through a symlink could
ever match. On macOS that is every temp directory — `os.tmpdir()` is
`/var/folders/...` and `/var` is a symlink to `/private/var` — so
`ruflo proxy install` rejected the binary it had just signature-verified.
Linux is unaffected because `/tmp` is real, which is why CI stayed green.
Keep both forms of every prefix and compare against the one matching how
the candidate was resolved. Canonicalizing the prefixes outright would have
broken `validateSync()` / `isWithinAllowed()`, which are documented as
lexical-only, in the mirror direction.
When the candidate's leaf does not exist, canonicalize its longest existing
ancestor instead of falling back to the lexical path — otherwise
`allowNonExistent` (the default) would regress under a symlinked prefix.
That also closes a hole this exposed: `<allowed>/escape/not-written-yet`,
where `escape` symlinks outside the prefix, was previously accepted because
`realpath` threw ENOENT on the missing leaf.
Escapes stay blocked, and the regression tests construct the symlink
explicitly rather than relying on the macOS quirk, so they hold on every
platform. 3 of the 6 fail without this change.
Co-Authored-By: RuFlo <ruv@ruv.net>
* fix(security): keep root prefixes usable and leave strict results untouched
Three defects found by an adversarial review of the previous commit.
`allowNonExistent: false` was still canonicalizing the candidate on ENOENT.
The call already fails, so the only effect was rewriting the `resolvedPath`
and `matchedPrefix` reported back — a silent shape change for any caller
reading those off a rejected result. Canonicalize only when non-existent
paths are actually allowed.
A root prefix (`/`, `C:\`, a UNC share root) already ends in a separator, so
anchoring the boundary as `prefix + path.sep` searched for `//` and rejected
every descendant. Pre-existing at all three comparison sites — this change
only widened how a prefix can end up being a root, since one can now
canonicalize to it. Extracted `isWithinPrefix`, which keeps the boundary
anchored so `/srv/app-secrets` is still outside `/srv/app`.
The guarded symlink tests returned early instead of skipping, so they
reported as passed on a host that cannot create links, and the flag was
never reset between cases. They now skip properly and reset per case.
Also documents that prefixes are canonicalized once, at construction: the
allowlist keeps denoting what it named when configured, rather than handing
whoever can rewrite a link the ability to redirect it later.
Co-Authored-By: RuFlo <ruv@ruv.net>
---------
Co-authored-by: RuFlo <ruv@ruv.net>
PathValidator.validate() canonicalizes the candidate path through
fs.realpath (when resolveSymlinks is on, the default), but the constructor
only ever path.resolve()d the allowed prefixes — never realpath'd them. On
any platform where a prefix is itself reached through a symlink (e.g.
macOS os.tmpdir() under /var, itself a symlink to /private/var), the
realpath'd candidate can never match the non-realpath'd prefix, and every
path under that prefix — including the prefix's own contents — is
rejected as "outside allowed directories".
This broke `ruflo proxy install` on macOS: the download and Ed25519
signature verification both succeed, then the defense-in-depth path check
in proxy/install.ts fails on the extracted binary, which lives under a
workDir created via os.tmpdir().
Fix: realpath the prefixes at construction time too, with the same
ENOENT-tolerant fallback to path.resolve() that validate() already uses
for candidates (a prefix directory that doesn't exist yet degrades
gracefully instead of throwing).
Implements all 3 phases of ADR-377 (ruvnet/ruflo#2516, tracked in #2873):
AgentDB's retrieval and write paths had zero certified defenses against
memory poisoning — SMSR (arXiv:2606.12703) shows 93-100% undefended attack
success. All three phases are off by default; enabling each is opt-in via
env flag until independently benchmarked, per the ADR's own gate criteria.
Phase 1 — AgentDbRetrievalGuard (@claude-flow/memory)
- New `agentdb-retrieval-guard.ts`, wired into `AgentDBBackend.search()`
(covers `query()`'s semantic path too, since it calls `search()`
internally). Reuses `@claude-flow/security`'s `ToolOutputGuardrail`
(ADR-131) for the actual OWASP LLM01/LLM08 pattern scan rather than
duplicating it — `@claude-flow/memory` now depends on
`@claude-flow/security` (no cycle: security has zero workspace deps).
- Size gate + guardrail scan per retrieved chunk. Flag-only by default;
`CLAUDE_FLOW_RETRIEVAL_GUARD_STRICT=true` drops unsafe/oversized chunks
instead of just annotating them. Both flags off by default
(`CLAUDE_FLOW_RETRIEVAL_GUARD=true` to enable scanning at all).
Phase 2 — MemoryPoisonForensics (@claude-flow/hooks)
- New `workers/memory-poison-forensics.ts`: rolling-window z-score anomaly
detection on write-interval and content-length features per
(agentId, namespace), reproducing the Forensic Trajectory Signatures
(arXiv:2606.30566) shape (behavioral features on write *sequences*, not
content). Opt-in `HookEvent.PostToolUse` handler for memory-write tools;
`CLAUDE_FLOW_POISON_FORENSICS=0` disables recording.
- NOT wired into any of this repo's three separate "worker" dispatch
systems (hooks' own workers/index.ts codebase-analyzers,
@claude-flow/swarm's worker-dispatch.ts 12-trigger table, or the CLI's
scheduled daemon) — @claude-flow/swarm is a deliberately
zero-workspace-dependency package and adding hooks there would break
that property; picking the wrong one of three ambiguous dispatch tables
seemed worse than leaving that integration as an explicit follow-up.
See the file header for detail.
Phase 3 — McpCallerIdentity (@claude-flow/security)
- New `mcp-caller-identity.ts`: per-invocation Ed25519-signed capability
tokens (callerId + toolName + nonce + 30s TTL by default), native
node:crypto sign/verify following the same pattern already used by
`@claude-flow/browser`'s witness-signer.ts (reimplemented rather than
cross-imported — a security-primitives module depending on the browser
package would be a backwards layering dependency).
- Standalone issue/verify API; deliberately NOT wired into
`AgentAuthorizationPropagator.checkToolCall` (ADR-144) or any tool
dispatcher — this repo has at least 3 distinct `ToolRegistry`
implementations and guessing which is authoritative risked silently
wiring the wrong path. `CLAUDE_FLOW_MCP_CALLER_AUTH` defaults to
disabled, matching the ADR's own text: key distribution needs a
follow-on ADR before this ships enabled.
Deviations from the ADR's guessed file paths/class names (documented
inline where relevant): the ADR was written against aspirational paths
(`agentdb/retrieval-guard.ts`, a `BackgroundWorker` interface,
`SafeExecutor`) that don't match the real repo layout — implementation
targets the actual `AgentDBBackend`, the actual worker systems, and the
actual `AgentAuthorizationPropagator`/tool-registries respectively.
Tests: 31 new tests across the three packages (9 + 11 + 11), all passing;
full existing suites re-run clean (memory 451, hooks 145, security 570 —
zero regressions).
Adds the live Ruflo capability brain, governed federation and Cognitum product planes, source-bound concurrent harness contracts, ADRs 325-329, and the v3.32.28 guarded release train.
* feat(memory): typed provenance in AgentDB (ADR-323)
Corrects and implements the 2026-07-28 dream-cycle research proposal
(PR #2804, closed as superseded) for typed memory provenance in
AgentDB, addressing MemIR/MemSyco-Bench's "provenance-role collapse"
and memory-induced sycophancy findings.
The dream-cycle PR's proposed schema change targeted the wrong table
(`vector_indexes`, which is per-namespace HNSW index metadata, not
per-entry) and its filename collided with the already-merged
`ADR-322-metaharness-flywheel-integration.md` (#2817) — both corrected
here as ADR-323 against `memory_entries`, the real per-record table.
- memory-initializer.ts: `provenance_type` column (user_claim |
agent_output | system_observation | tool_result | unknown, default
unknown) on `memory_entries`, migrated for existing DBs via
`ensureSchemaColumns()`. `storeEntry()`/`searchEntries()` accept and
validate `provenanceType`/`provenanceFilter`.
- memory-bridge.ts: same wiring for the native better-sqlite3 AgentDB
v3 path (`bridgeStoreEntry`/`bridgeSearchEntries`).
- commands/memory.ts: `memory store --provenance <type>`, `memory
search --provenance-filter <types>` (comma-separated, OR semantics).
- mcp-tools/memory-tools.ts + cli-core/mcp-tools/memory-defs.ts:
`memory_store`'s `provenance_type` param, `memory_search`'s
`provenance_filter` param (kept in sync across both tool-def sets).
Found and fixed during implementation: an initial design skipped the
RaBitQ/HNSW acceleration paths entirely whenever a provenance filter
was requested (neither carries provenance_type on their candidates).
This reproducibly crashed the CLI process on exit (a libuv assertion
on Windows) after printing correct results. Fixed by never skipping
those paths — RaBitQ now fetches provenance_type in its existing
per-candidate query, HNSW does one batched (namespace, key) lookup
afterward — eliminating the crash across repeated runs. See ADR-323's
"Implementation note" for detail.
7 new end-to-end tests (adr-323-memory-provenance.test.ts), including
a repeated-run regression guard for the crash. Full existing memory
suite re-verified with no regressions (one pre-existing, environment-
specific #2558 test failure confirmed identical against unmodified
main — better-sqlite3's native bridge is unavailable in this dev
environment, unrelated to this change).
Closes the follow-up from dream-cycle PR #2804 (closed as superseded).
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(memory): enforce provenance across retrieval paths
* feat: add agentic policy engine
* fix: align Codex MCP integration
* fix: keep MetaHarness dependencies optional
* fix: declare MetaHarness as optional peers
* fix: sync workspace lockfile
* fix: document policy tool selection guidance
* test: cover governed in-process flywheel
* ci: make MetaHarness absence drill deterministic
Follow-up from dream-cycle issue #2783 / PR #2784. Implements the v2 that the already-shipped v1 (commits 381b7ebcc/581cd2bf3) explicitly deferred: SimHash-based cross-tool fragment detection for MCP composition, and a ChannelGuard reusing the real InputValidator instead of a reinvented catalog. 29 new tests, 134/134 existing hooks tests passing, 0 regressions, clippy clean.
Three independent P1 components, each scoped to a single PR-sized change.
All three are off by default — flips become default in v4.0 — so existing
pipelines keep their exact behaviour.
ADR-144 P1 — AgentAuthorizationPropagator (#2248)
v3/@claude-flow/security/src/authorization/propagator.ts (new)
• AuthScope envelope (principal, granted tools/servers, delegation depth, expiry)
• wrapOutbound: monotonically-reducing scope, depth-decrement, expiry check
• checkToolCall: typed decisions (tool-not-in-scope / server-not-in-scope /
scope-expired / delegation-depth-exhausted) — never throws, telemetry-friendly
• verifyServerAuth: fail-closed on missing/empty credentials (P1 permissive
accept for non-empty — P4 wires real validator)
• Provenance buffer ring-bounded for P5 telemetry sink
• makeLegacyPermissiveScope shim for legacy-mode migrations
18 unit tests covering every invariant; all pass.
ADR-145 P1 — PluginIntegrityVerifier (#2254)
v3/@claude-flow/security/src/plugins/integrity-verifier.ts (new)
v3/@claude-flow/cli/src/plugins/trust/trust-anchors.json (new placeholder)
• Canonical JSON serialisation (deterministic key order) + SHA-256 manifest hash
• Ed25519 detached signature verification via @noble/ed25519 (probe-and-fall-
back, mirrors verify.mjs #1880 pattern so untrusted environments skip
rather than throw)
• Trust-anchor allowlist with exact / wildcard scope matching + expiry
• Structured VerificationStatus (pass / signature-missing / signature-invalid
/ manifest-hash-mismatch / unknown-signer / signer-expired)
• Stage-2 semantic-intent scan (SCH defence) lands in P2
13 unit tests including the round-trip sign→verify and tamper-flip cases.
ADR-146 P2 — guardrail call site in MCP dispatch (#2149 follow-up)
v3/@claude-flow/cli/src/mcp-client.ts — patch callMCPTool
• Lazy-resolves @claude-flow/security so cold-import cost doesn't hit
every CLI invocation; falls back to no-op if module unavailable
• Walks the result object one level deep (matches the flat-record shape
of every existing tool — deeper traversal would change the p99 latency
contract)
• Rejected fields replaced with typed marker: '<rejected-by-guardrail
tool="X" category=Y>' so callers can surface the rejection rather than
silently dropping
• Off by default. CLAUDE_FLOW_STRICT_GUARDRAIL=true turns it on; precedence
documented inline so the env-var audit passes without an escape-hatch entry.
4 wiring tests (legacy passthrough, strict-mode reject of known injection,
strict-mode passthrough on safe content, non-object results pass through).
Security index exports updated for all three. Builds clean (tsc with no
errors). All 4 audits + 2 regression smokes pass locally.
Co-Authored-By: RuFlo <ruv@ruv.net>
* feat(security): #2149 ADR-131 P1 — ToolOutputGuardrail + OWASP 2026 mapping
Closes the OWASP ASI01 (Agent Goal Hijacking) gap identified in the
2026-05-26 dream cycle. Adds a pure, synchronous, pattern-based screener
for content crossing the agent's content boundary (MCP tool results,
memory reads, external API responses) before it enters reasoning.
Design (full rationale in ADR-131):
- Eight built-in pattern categories: instruction-override, embedded-system
(ChatML / Llama [INST] / <system>), exfiltration, role-hijack, jailbreak,
hidden-unicode (bidi + zero-width), tool-spoofing, truncation
- Default policy: critical→reject, high→redact, medium→flag, low→allow
- Pure-function shape — safe to invoke in every tool result / memory read
- 32KB content scans in 0.1ms locally (target: <1ms p99)
Scope:
- P1 (this PR): class + tests + exports + ADR + OWASP matrix + CI smoke
- P2-P5 (follow-on): MCP dispatch, memory read, swarm Raft payload,
per-tool policy + telemetry — tracked in ADR-131
Validation:
- 24/24 vitest tests pass (9ms)
- 11/11 smoke checks against built dist/index.js
- Tested against the four canonical ASI01 attack shapes
- CI guard `tool-output-guardrail-smoke` wired into v3-ci.yml
OWASP Top 10 for Agentic Applications 2026 control matrix added:
v3/docs/security/owasp-agents-2026-mapping.md — flags ASI01/06/09/10 as
highest-priority remaining gaps.
Co-Authored-By: RuFlo <ruv@ruv.net>
* fix(plugins/ruflo-core/hooks): #2155 — wrap 3 unwrapped .sh hooks in /bin/bash -c
Three hooks (PreToolUse:Bash, PreToolUse:Write|Edit|MultiEdit, Stop)
invoked scripts/ruflo-hook.sh directly without an outer shell wrapper.
On Windows, Claude Code's hook executor spawned the .sh path, Node read
the shebang, attempted to exec /bin/bash (absent on Windows), and exited
with code 126 — surfacing as PostToolUse:Bash/PreToolUse:Bash/Stop hook
errors in every user session.
Wrapped each invocation in /bin/bash -c '... || true' to match the
existing pattern of the four other hooks in the same file. The || true
fallback now executes inside the bash subshell rather than relying on
the .sh body being readable. Validated as still working on POSIX.
Co-Authored-By: RuFlo <ruv@ruv.net>
1. SQL injection: Convert all 9 string-interpolated queries in
memory-initializer.ts to parameterized queries (db.prepare/bind)
Closes#1030
2. Preinstall hook: Remove no-op preinstall script and package.json entry
that triggered supply chain concern
3. SECURITY.md: Add vulnerability disclosure policy with supported
versions, reporting process, and safe harbor statement
4. SafeExecutor: Remove npx from default command allowlist to prevent
arbitrary package execution via safe executor
5. Cleanup command: Add `cleanup` command for removing project artifacts
(.claude/, .swarm/, data/, etc.) with --dry-run default and --force
to execute. Addresses uninstall documentation gap
Published as v3.5.40. All fixes verified in Docker.
Closes#1375