* dream(security): #3151 sliding-window reset for maxToolCallsPerTurn (evaluated, ACCEPT) Follow-up to PR #3139 (2026-08-31, unmerged): the opt-in MCP policy enforcer's maxToolCallsPerTurn was a permanent session-lifetime cumulative counter that never reset, explicitly disclosed as unsafe to enable by default. Tonight's research (5 parallel roles) converged on a wall-clock sliding window as the correct fix, corroborated by the MCP spec's own silence on rate-limit mechanism, SEP-2567's removal of the session concept, Cloudflare's production sliding-window-counter data, and how FastMCP/PolicyLayer solve the same problem in the MCP ecosystem. - checkAndRecordCall/evaluateToolCall now take an injectable `now` (defaults to Date.now(), zero production behavior change at the one real call site in mcp-server.ts). - New McpPolicy.turnWindowMs field (default 60000ms); shipped in .harness/mcp-policy.json alongside the existing maxToolCallsPerTurn. - 30/30 tests passing (24 pre-existing unmodified + 6 new). Broader MCP-suite sweep: 52/56 (4 failures are pre-existing unbuilt-sibling- package environmental gaps, unrelated to this diff). - Independent adversarial critique: CONFIRMED-WITH-CAVEATS. Two minor findings (untested exact window boundary; audit-log timestamp cosmetic desync from the injected clock) fixed before this commit. Full report: docs/dream-cycle/dream-gist-2026-09-01.md * dream(security): #3151 update LEDGER.md for 2026-09-01 Ledger row for tonight's dream-cycle: sliding-window reset for maxToolCallsPerTurn (PR #3152, stacked on #3139). * dream(security): #3151 address ruvnet's PR #3152 review (round 1) Retargeted PR #3152's base to main: dream/2026-08-31-security is not main/develop, so every required CI workflow (ci.yml, integration-tests.yml, cve-audit.yml, v3-ci.yml, verification-pipeline.yml) is configured to trigger only on PRs targeting main/develop and structurally never ran on either #3139 or this stacked PR's head, regardless of what was pushed. This is a repo-wide trigger-configuration fact, not fixable by a commit. Also addresses the review's other concrete asks: - Built the two sibling packages disclosed as unbuilt (@claude-flow/cli-core, @claude-flow/neural) via tsc. Broader MCP-suite sweep improves from 52/56 to 201/204 -- remaining 3 failures confirmed (independently, by testing a raw ::1 bind outside any test) to be EAFNOSUPPORT: IPv6 loopback binding unsupported at the container/OS level in this sandbox, unrelated to this diff and not fixable by any code change here. - Added an integration test exercising the real stdio dispatcher (MCPServerManager.handleMCPMessage) through a full deny -> expiry -> allow cycle with fake timers, proving the fix holds without bypassing audit logging or the opt-in flag -- the review's specific ask, previously only covered at the direct evaluateToolCall() call level. - Strengthened documentation (McpPolicy.maxToolCallsPerTurn JSDoc and .harness/mcp-policy.json's own field comment): explicit that despite the field name this is wall-clock rate limiting, not a literal per-conversational-turn counter -- MCP has no protocol-level turn concept. 31/31 policy-enforcer tests passing (was 30/30; +1 dispatcher-level test). --------- Co-authored-by: Claude <noreply@anthropic.com>
.harness/ — ruflo's MetaHarness lifecycle directory
This directory holds the files that harness <subcommand> from metaharness reads to assess ruflo's governance posture. See ADR-150 for context.
Contents
| File | Purpose | Iter |
|---|---|---|
mcp-policy.json |
MCP governance policy. ADR-022 default-deny, audit-log on, dangerous-pattern list, per-turn call budget. Required to clear the no-policy HIGH finding from harness mcp-scan. |
30 |
manifest.json |
Hand-authored harness identity manifest: hosts + sha256 fingerprints of the security-relevant files (mcp-policy + .claude/settings). Required input for harness sign and for OIA-manifest's mcpPolicyPath field. |
32 |
witness.json |
NOT YET CREATED. Ed25519-signed Merkle-style witness over the manifest. Would push OIA governance from "full / mcp-policy.json (witness missing)" to "full / mcp-policy.json + witness ADR-011". See below. | — |
Witness — what's needed to ship it
harness sign . produces a witness from .harness/manifest.json BUT requires WITNESS_SIGNING_KEY (64-char hex Ed25519 private key) in the environment. Ruflo's existing brain-signing-key in GCP Secret Manager is in a different format (89 chars, not hex) and can't be reused.
To enable witness signing, one of:
-
Create a new GCP secret
witness-signing-key(32 random bytes, hex-encoded). Fetched via Workload Identity Federation in CI; locally viagcloud secrets versions access. Pros: same auth path as the existing OPENROUTER_API_KEY workflow. Cons: requires explicit user authorization (creates a new sensitive secret). -
Use a hardware key (YubiKey, Sigstore-style ephemeral identity). Pros: no long-lived private key on disk. Cons: more CI plumbing.
-
Skip the witness and accept the "partial witness" note in the OIA manifest. The threat-model and mcp-scan are already CLEAN without it; the witness only signs the manifest, it doesn't itself add new security controls.
Current state (option 3) gives ruflo:
- ✓ L7 governance alignment: full (per
harness oia-manifest) - ✓ threat-model worst severity: info / clean
- ✓ mcp-scan: 0 actionable findings
- The implementation note:
"mcp-policy.json (witness missing)"
The witness is genuinely optional unless an external party needs to verify the manifest hasn't been tampered with. For ruflo's internal use it's nice-to-have polish.
Verifying ruflo's current posture
# Full threat report
npx -y -p metaharness@latest harness threat-model .
# Per-server MCP scan
npx -y -p metaharness@latest harness mcp-scan .
# Full OIA layer alignment
npx -y -p metaharness@latest harness oia-manifest .
# Or via ruflo's wrappers (no metaharness install required if optional dep is present)
npx ruflo metaharness threat-model
npx ruflo metaharness mcp-scan
npx ruflo metaharness oia-audit --dry-run
Updating mcp-policy.json
If you change mcp-policy.json, regenerate the manifest's sha256 entry:
node -e "
const { readFileSync, writeFileSync } = require('node:fs');
const { createHash } = require('node:crypto');
const m = JSON.parse(readFileSync('.harness/manifest.json', 'utf-8'));
m.files['.harness/mcp-policy.json'] = createHash('sha256').update(readFileSync('.harness/mcp-policy.json')).digest('hex');
m.files['.claude/settings.json'] = createHash('sha256').update(readFileSync('.claude/settings.json')).digest('hex');
m.generated_at = new Date().toISOString();
writeFileSync('.harness/manifest.json', JSON.stringify(m, null, 2) + '\n');
console.log('Manifest updated.');
"
Smoke step 17t (in plugins/ruflo-metaharness/scripts/smoke.sh) locks the policy invariants. If you change the policy in a way that violates defaultDeny:true / auditLog:true / requireApprovalForDangerous:true or removes the toolTimeoutMs/maxToolCallsPerTurn positive-number requirements, CI will fail.