Routing constraints are now inviolable — data-policy and provider-deny survive filter starvation
2026-08-05
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:
filterEndpoints()— constraints plus the relaxable checks (breaker, context window).- The starvation-recovery path — relaxes breaker state to half-open, constraints held.
- 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".