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.

ModeBehavior
offEnvelope ignored. Original behavior preserved. Default.
warnGate computes the decision. Logs what it would do. Does not enforce.
enforceGate 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):

  1. br_trust.xdr_risk >= 0.7 → force-downgrade to restricted-equivalent

regardless of nominal tier. External XDR sourced outside BR has the highest reliability and is evaluated first.

  1. br_trust.anomaly_score >= 0.8 → downgrade by one tier (gold → silver,

etc.). Evaluated when XDR didn't fire.

  1. br_trust.tier → look up in TIER_STRATEGY when 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() → 403 budget_exceeded before Redis is consulted
  • br_budget.cap_usd <= br_budget.spent_usd → 403 budget_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:

ConditionEffective PII mode
tier === "restricted" or xdr_risk >= 0.5block
tier === "bronze" or anomaly_score >= 0.7max(configured, redact)
Otherwiseconfigured (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:

TierDefault routing strategyDefault PII mode escalationNotes
restrictedprice (cheapest)blockQuarantine equivalent. Cert lifetimes <60s.
bronzepriceredact (if currently less strict)New agents start here. Earn promotion via clean call history.
silverunconstrainednoneDefault for established agents.
goldunconstrainednoneLong-lived cert (5–10 min). Higher rate limits.
platinumunconstrainednoneReserved 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:

  1. 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.

  1. 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).

  1. 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