Routing constraints are now inviolable — data-policy and provider-deny survive filter starvation

2026-08-05

routergovernancesecurity

LOCKSTEP TRACEABILITY MATRIX --- api_endpoints: ["none"] sdk_methods_updated: ["none"] mcp_tools_updated: ["none"] ---

What We Built

The router narrows candidate endpoints in two phases: filter, then sort. When the filter eliminated every candidate, a recovery path re-ran a looser filter so the request could still be served. That relaxed filter kept only circuit-breaker state and the capability gate — it silently dropped dataPolicy and providerDeny.

The consequence: a tenant who set dataPolicy: "zero" could, under a starved candidate pool, be routed to an endpoint that retains and trains on their data. A tenant who explicitly denied a provider could be routed to that provider. The event was recorded as a filter_relax routing stage, so it was visible after the fact — but the commitment had already been broken.

This change introduces an explicit distinction between preferences and constraints. Preferences (strategy, cost ceiling, priority, circuit-breaker state, context window) degrade gracefully under pressure. Constraints (provider allow/deny, required capabilities, data-retention policy) do not degrade at all — a request that cannot be served within its constraints now fails instead of routing somewhere non-compliant.

Why It Matters

Every governance claim BrainstormRouter makes rests on constraints actually holding. We emit tamper-evident decision lineage — a lineageDigest chained into each audit row's provenanceDigest. That evidence layer is an asset only while the constraints it records were genuinely enforced. A cryptographically signed record of a violated data policy is not a neutral artifact; it is a durable, timestamped, discoverable record of a breach.

The stronger the evidence chain, the more expensive a silent constraint leak becomes. This fix is the precondition for everything built on top of it.

How It Works

checkEndpointConstraints(endpoint, ctx) is now the single definition of a hard constraint. It returns null when satisfied, otherwise a human-readable reason:

if (prefs?.dataPolicy === "zero" && ep.dataRetention !== "zero") {
  return `data policy "zero" requires a zero-retention endpoint (endpoint retention: "${ep.dataRetention}")`;
}

All three code paths that narrow candidates now call it:

  1. filterEndpoints() — constraints plus the relaxable checks (breaker, context window).
  2. The starvation-recovery path — relaxes breaker state to half-open, constraints held.
  3. Strict-mode failure classification — previously a hand-copied duplicate of the policy

list, now collapsed to the shared function.

Returning a _reason_ rather than a boolean is deliberate: those strings are the raw material for constraint-level evidence in the decision trace, where the artifact becomes "the eligible set was {A,B,C}; D and E were excluded because …" rather than merely "we used model B".

The Numbers

  • 2 constraint classes were leaking on the relaxed path (dataPolicy, providerDeny).
  • 3 enforcement points unified behind one function; 1 duplicated policy block deleted.
  • Verified by reverting the fix in place: both new regression tests fail against the old

behavior with "Should have thrown rather than routing to a non-compliant endpoint", and pass against the new. The pre-existing capability-gate test passes in both, confirming the capability gate was never the leak.

  • Full unit suite: 9219 passed, 0 failures.

Competitive Edge

Portkey (now Palo Alto Prisma AIRS) and OpenRouter both ship governance as _controls_ — spend caps, zero-data-retention settings, guardrails. Controls prevent; they do not prove. Credo AI produces audit-ready evidence but has no runtime and cannot enforce anything at inference time.

Enforcing a constraint inside the routing decision _and_ emitting per-request evidence that it held is the intersection neither side occupies. This change makes the enforcement half true under adversarial conditions — starvation, outage, degradation — which is exactly when a gateway is most tempted to relax and exactly when an auditor will look.

Lockstep Checklist

  • [x] API Routes: no API surface changed — router-internal behavior only.
  • [x] TS SDK: no change required (no new request or response fields).
  • [x] Python SDK: no change required (no new request or response fields).
  • [x] MCP Schemas: no change required (not agent-facing).
  • [x] Master Record: docs/architecture/master-capability-record.mdx — "Routing constraint

enforcement" added to Tier 1, and the preferences-degrade/constraints-do-not rule recorded under "Router invariants".