Trust Envelope
A signed JWT that carries every routing, budget, and guardrail decision input on every request — observable in audit-only mode, enforceable when you flip a config flag.
> Normative spec: /specs/trust-envelope-v1 — RFC-style claim definitions, verification rules, and security considerations. This page is the narrative companion.
What it is
A trust envelope is a signed JSON Web Token (JWS, EdDSA / Ed25519) that BrainstormRouter mints on every request after auth. It bundles the principal identity, budget posture, allowed scope, trust signals, observability requirements, and test/sandbox markers into a single payload that downstream gates read instead of querying the auth row, budget store, reputation engine, and anomaly engine separately.
The envelope is the single decision contract for routing, budget, and guardrails. It is sourced once, signed once, and every gate downstream reads the same view — eliminating the class of bugs where two gates disagree because one read fresh state and one read cached state.
It is JOSE-aligned (RFC 7519 / RFC 8725) so future IETF agent-JWT drafts and W3C VC 2.0 issuance are additive, not breaking.
Schema
type TrustEnvelope = {
// Standard JWT claims
iss: "brainstormrouter";
sub: string; // SPIFFE URI for agents/mTLS, "user:{id}" for humans, "tenant:{id}" fallback
iat: number;
exp: number;
jti: string;
br_principal: {
agent_id: string | null;
user_id: string | null;
org_id: string;
parent_chain: { type: "agent" | "user" | "system"; id: string; ts: number }[];
auth_method: "api_key" | "agent_jwt" | "mtls" | "supabase_jwt";
};
br_budget: {
period: "request" | "session" | "day" | "month";
cap_usd: number;
spent_usd: number;
hard_stop_at: number; // ms epoch — request rejected after this instant
};
br_scope: {
providers: string[]; // [] = no provider restriction
models: string[] | "*"; // "*" = no model restriction
tools: string[] | "*"; // "*" = no tool restriction
regions: string[] | "*";
};
br_trust: {
tier: "platinum" | "gold" | "silver" | "bronze" | "restricted";
mtls_fingerprint: string | null;
attestation_hash: string | null; // TEE attestation, reserved
anomaly_score: number; // 0..1 from in-process anomaly engine
reputation: {
successful_calls: number;
failed_calls: number;
last_anomaly_at: number | null;
};
xdr_risk?: number; // 0..1 from external SIEM/XDR (X-7)
};
br_observability: {
trace_required: boolean;
fields_to_capture: string[];
retention_days: number;
redaction_policy: "none" | "pii-redacted" | "full-redacted";
};
br_test: {
tier: "production" | "sandbox";
isolation_marker: string | null;
};
};
The Zod validator TrustEnvelopeSchema lives in src/security/trust-envelope/schema.ts. Any decoded envelope round-trips through the schema before a gate reads it.
How requests carry it
The synthesizer middleware mints the envelope after the auth chain (api-key / agent-JWT / mTLS / Supabase-JWT) resolves the principal. Every response includes:
X-BR-Envelope: audit— hint header that the audit pass is live (the value
is the mode, never the token — the signature would leak otherwise).
- The signed token itself never leaves the gateway in A-1; it lives on the
Hono request context as c.get("trustEnvelope") and downstream gates read the decoded view via c.get("_trustEnvelopeDecoded").
When BR proxies to a downstream service that participates in the trust mesh (MSP, brainstorm-agent), the envelope can be propagated as a JOSE-compatible JWT in Authorization: Bearer — but A-1 keeps the envelope in-process only.
Three-mode pattern
Every envelope-load-bearing system has three modes. This is deliberate — the envelope is wired ahead of enforcement so on-call can measure synthesis overhead and false-positive rates before flipping to enforce.
| Mode | Behavior |
|---|---|
off | Envelope ignored. Original behavior preserved. Default. |
warn | Gate computes the decision. Logs what it would do. Does not enforce. |
enforce | Gate applies the decision. Returns 403/blocks/redacts as appropriate. |
Synth has a fourth mode — audit-only — equivalent to warn for the synthesizer (envelope is minted and logged, but no downstream consumer is forced to read it).
Configure each independently in gateway.envelope.{system}.mode:
envelope:
synth:
mode: audit-only # or off
routing:
mode: off # off | warn | enforce
budget:
mode: off
guardrails:
mode: off
gateway.* is restart-class config — changes require a task restart, not a hot-reload.
What each gate reads
Routing (A-3) — src/security/trust-envelope/routing-gates.ts
applyTrustGates(envelope, candidates, mode) filters the candidate endpoint set and may override the routing strategy.
Signal precedence (fixed order, first match wins):
br_trust.xdr_risk >= 0.7→ force-downgrade torestricted-equivalent
regardless of nominal tier. External XDR sourced outside BR has the highest reliability and is evaluated first.
br_trust.anomaly_score >= 0.8→ downgrade by one tier (gold → silver,
etc.). Evaluated when XDR didn't fire.
br_trust.tier→ look up inTIER_STRATEGYwhen neither XDR nor
anomaly fired:
restricted/bronze→ force"price"strategy (cheapest models)silver/gold/platinum→ no override (standard bandit / quality routing)
br_scope.providers and br_scope.models are applied as candidate filters before any of the three signal gates run. The result includes a source field — one of "xdr_risk", "anomaly", "tier", or null — that names the signal that acted. The label is "anomaly", not "anomaly_score", to match the gate variable name. Logged on every routing decision so the forensic trail identifies the cause.
Budget (A-4) — src/api/middleware/budget.ts
budgetMiddleware(opts) accepts envelopeBudgetMode. In enforce:
br_budget.hard_stop_at <= now()→ 403budget_exceededbefore Redis is consultedbr_budget.cap_usd <= br_budget.spent_usd→ 403budget_exceeded
These checks run before Redis so a stale or unreachable Redis cache cannot let a known-over-budget request through.
Guardrails (A-5) — src/api/middleware/guardrails.ts
Envelope claims escalate the configured PII / content-filter mode:
| Condition | Effective PII mode |
|---|---|
tier === "restricted" or xdr_risk >= 0.5 | block |
tier === "bronze" or anomaly_score >= 0.7 | max(configured, redact) |
| Otherwise | configured (no escalation) |
The escalation is logged with a precise reason (xdr_risk=0.62 >= 0.5 or tier=restricted) so on-call can attribute every escalation to a specific signal.
Reputation tiers
Five tiers, ordered from most-restrictive to most-permissive:
| Tier | Default routing strategy | Default PII mode escalation | Notes |
|---|---|---|---|
restricted | price (cheapest) | block | Quarantine equivalent. Cert lifetimes <60s. |
bronze | price | redact (if currently less strict) | New agents start here. Earn promotion via clean call history. |
silver | unconstrained | none | Default for established agents. |
gold | unconstrained | none | Long-lived cert (5–10 min). Higher rate limits. |
platinum | unconstrained | none | Reserved for cross-tenant warm-start sources + production fleet. |
Promotion is reputation-driven (successful calls vs failed calls vs anomaly flags) — see Agent Reputation for the quantitative thresholds and the engine that records outcomes.
Synthesis cost
Audit-only synthesis adds 6–16 ms p99 to the cold-start path (trustEnvelope=6.1ms to trustEnvelope=15.5ms in [COLD-START] traces), warm-path is sub-1ms because key material is cached. The signing key is RSA-backed for compatibility with CAF mTLS but the envelope itself uses Ed25519 (EdDSA) for performance.
Failure modes
A-1 is fail-open by design — if synthesis or signing fails, the envelope is set to null, the failure is logged with grep-able context (path, authMethod, tenantId, requestId), and the request proceeds. The audit pass must not take prod traffic down.
A-3+ flips the failure mode to fail-closed when enforcement engages: a synth failure in enforce mode results in a 503 envelope_unavailable response. This is the right tradeoff once gates are load-bearing — a request the gateway cannot reason about should not reach a model.
Why not just read the auth row + budget store + reputation engine?
Three reasons:
- Decision consistency. Two gates that read the same envelope agree by
construction. Two gates that each query state separately can disagree because the state changed between reads — the [reputation tier was updated, the budget was just over-spent, an anomaly just fired]. The envelope freezes the decision inputs at synth time so every gate sees the same world.
- Audit trail. Every request gets one signed payload with
jti. The
audit chain links these by jti so any gate decision is replayable — "what would have happened if this request had carried envelope X" becomes a tractable question (see counterfactual replay).
- Cross-service trust mesh. When BR delegates to MSP or brainstorm-agent,
the envelope IS the propagation format. The JOSE alignment means future W3C VC 2.0 issuance + IETF agent-JWT (klrc, sharif, goswami, singla drafts) become additive — same shape, different signing/verification semantics.
Related concepts
- Agent Reputation — the 5-tier system, signal mix, promotion logic
- Graduated Trust — runtime anomaly-driven trust, feeds
br_trust.anomaly_score - XDR Pipeline — external SIEM signals, source of
br_trust.xdr_risk - Agent Identity — how
br_principalis resolved per auth method