Track B Phase 3 (CR-3) — would-deny preview (warn-mode, deduped, lookback)

2026-06-21

scope-enforcementaudit-chaingovernance-dashboard

LOCKSTEP TRACEABILITY MATRIX --- api_endpoints: ["GET /auth/governance/would-deny"] sdk_methods_updated: ["none — dashboard JWT surface"] mcp_tools_updated: ["none"] ---

What We Built

GET /auth/governance/would-deny?days=N answers the question that gates every "flip warn → enforce" decision: what would enforcement have blocked? It reads the warn-mode denials (audit outcome = warned) over an adjustable 1–90 day window and dedups them into reason × principal × count groups (R14) — a 200-event warn flood becomes one counted row, not a wall of amber — and returns the oldest-evidence horizon so an operator widening the window knows exactly how far back the data goes before trusting it (R15).

The dashboard renders it as a WOULD-DENY card on #grants with 1d/7d/30d/90d lookback buttons, the deduped table, and an explicit "oldest evidence available: N days" line.

Why It Matters

Warn mode is how a tenant safely adopts scope enforcement: run it as a preview, watch what _would_ break, then flip to enforce once the would-deny list is clean. Until now that evidence lived only in the apiKey audit chain. CR-3 surfaces it to the human operator, deduped and windowed, so the enforce-confirm flow can honestly say "these N would have been blocked — proceed?" instead of guessing.

How It Works

The route uses the existing outcome filter on the tenant-scoped completion-audit query, pulling warned rows since now − days, then groups by (reasonCode|violatedScope, principal) from the non-hashed policy_context, tracking each group's count + last-seen and the global oldest timestamp. No new table, no new write path — it reads the warn evidence Increment 2 already records.

The Numbers

  • 1 new JWT route; deduped to ≤50 groups; 1–90 day lookback. Backend typechecks + lints; 389 dashboard tests pass; pnpm check green.

Competitive Edge

"Dry-run your policy" is common. "Here are the exact 41 calls, by principal and reason, that your _scope_ policy would have blocked in the last 30 days, and the evidence goes back 12 days" — committed to a tamper-evident chain — is the evidence plane turning a risky cutover into a measured one.

Lockstep Checklist

  • [x] API Routes: GET /auth/governance/would-deny.
  • [x] TS/Python SDK / MCP: none — dashboard JWT surface.
  • [x] Generated contract: docs/openapi.yaml + static-assets regenerated.

Remaining (Phase 3 + 4)

Would-deny preview lands the CR-3 piece. Still ahead: effective-state badges (CR-2/CR-5), the scope-check composer (CR-4 refactor), the full chained Ledger view, and the /v1 chain-verify DRY migration. Phase 4 (Plans, CR-9) after.