Trust Envelope — v1

Normative schema for BrainstormRouter's EdDSA-signed agent identity JWT. Carries principal, budget, scope, trust, observability, and test/sandbox markers in a single signed payload.

Status: Draft 1 · published 2026-05-16 · canonical URL https://docs.brainstormrouter.com/specs/trust-envelope-v1

Authors: BrainstormRouter License: CC BY 4.0 (specification) — reference implementations under their respective package licenses

This specification is normative. The keywords MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are used per RFC 2119.

---

1. Abstract

The Trust Envelope is a JSON Web Signature (RFC 7515) that BrainstormRouter mints on every authenticated request. It bundles the principal identity, budget posture, allowed scope, trust signals, observability requirements, and test/sandbox markers into a single signed payload that downstream gates (routing, budget, guardrails) read instead of querying the auth row, budget store, reputation engine, and anomaly engine independently.

The envelope is the single decision contract for every downstream gate. It is sourced once, signed once, and every gate reads the same view — eliminating the class of bugs where two gates disagree because one read fresh state and one read cached state.

The envelope is JOSE-aligned (RFC 7519 / RFC 8725) so future IETF agent-JWT drafts and W3C VC 2.0 issuance are additive, not breaking.

2. Token format

A Trust Envelope is a JWS with:

2.1 Header

{
  "alg": "EdDSA",
  "typ": "JWT",
  "kid": "<envelope-key-id>"
}
  • alg: MUST be EdDSA (Ed25519). Other algorithms are out of scope for v1.
  • typ: MUST be JWT.
  • kid: MUST identify the active signing key from the issuer's JWKS.

2.2 Payload — top-level claims

ClaimTypeRequiredNotes
issstringyesMUST be the literal "brainstormrouter" for envelopes minted by BR
substringyesSPIFFE URI for agents/mTLS, user:{userId} for humans, tenant:{tenantId} fallback
iatnumberyesIssued-at, seconds since epoch
expnumberyesExpiry, seconds since epoch. exp - iat SHOULD be ≤ 300 (5 minutes)
jtistringyesRequest UUID, unique per envelope

2.3 Payload — br_principal

Identity, organization, and delegation lineage.

br_principal: {
  agent_id: string | null; // null when caller is human
  user_id: string | null; // null when caller is an agent
  org_id: string; // always present
  parent_chain: Array<{
    type: "agent" | "user" | "system";
    id: string;
    ts: number; // ms epoch when delegation occurred
  }>;
  auth_method: "api_key" | "agent_jwt" | "mtls" | "supabase_jwt";
}

Exactly one of agent_id or user_id MAY be null. The other MUST be set. org_id is always non-empty. parent_chain MAY be empty in v1.0 (delegation lineage lands in a later draft).

2.4 Payload — br_budget

Authoritative spend posture for the request.

br_budget: {
  period: "request" | "session" | "day" | "month";
  cap_usd: number; // non-negative
  spent_usd: number; // non-negative; MUST be ≤ cap_usd
  hard_stop_at: number; // ms epoch; request MUST be rejected after this instant
}

hard_stop_at is the deadline for the entire request, not just the budget window. Implementations MUST reject any operation that would extend the request past this timestamp.

2.5 Payload — br_scope

Allowlists. Wildcard semantics are explicit; empty arrays are NOT wildcards.

br_scope: {
  providers: string[];                       // [] = no provider restriction (v1.0 transitional)
  models:    string[] | "*";                 // "*" = wildcard; [] = DENY ALL
  tools:     string[] | "*";                 // "*" = wildcard; [] = DENY ALL
  regions:   string[] | "*";                 // "*" = wildcard; [] = DENY ALL
}

Empty array vs wildcard (locked 2026-05-13, issue #296):

  • For models, tools, and regions: an empty array [] MUST be treated as deny all. Wildcard is the literal string "*".
  • For providers: an empty array [] MAY be treated as "not yet load-bearing" in v1.0 (transitional semantics for envelopes minted before provider scope was enforced). The bridged token form (see MCP-OAuth Bridge §4.3) uses stricter deny-all semantics.

2.6 Payload — br_trust

Trust posture and risk signals.

br_trust: {
  tier: "platinum" | "gold" | "silver" | "bronze" | "restricted";
  mtls_fingerprint: string | null;
  attestation_hash: string | null;           // TEE attestation; reserved
  anomaly_score: number;                     // 0..1, in-process anomaly engine
  reputation: {
    successful_calls: number;
    failed_calls: number;
    last_anomaly_at: number | null;
  };
  xdr_risk?: number;                         // 0..1, optional external XDR signal
}

tier is the canonical 5-tier reputation enum. Implementations MUST NOT introduce additional tier values.

anomaly_score is computed by the issuer's in-process anomaly engine and reflects per-request signals. xdr_risk is sourced from external XDR systems and is optional; consumers SHOULD treat its absence as 0.0.

2.7 Payload — br_observability

Logging and audit requirements that downstream consumers MUST honor.

br_observability: {
  trace_required: boolean;
  fields_to_capture: string[];               // e.g., ["model", "tool", "cost"]
  retention_days: number;                    // non-negative integer
  redaction_policy: "none" | "pii-redacted" | "full-redacted";
}

Consumers MUST apply redaction_policy BEFORE writing to storage. Logging fields outside fields_to_capture is permitted (e.g., for debugging) but those fields MUST NOT be retained past retention_days.

2.8 Payload — br_test

Sandbox isolation markers.

br_test: {
  tier: "production" | "sandbox";
  isolation_marker: string | null; // tag for filtering sandbox traffic from dashboards
}

Envelopes with tier: "sandbox" MUST NOT be charged against production budgets or counted in production observability metrics. Implementations SHOULD honor isolation_marker as an opaque filter tag.

3. Signing

Envelopes MUST be signed with EdDSA using an Ed25519 keypair. The issuer publishes its public key set via JWKS at:

GET https://api.brainstormrouter.com/.well-known/jwks.json

Consumers MUST refresh the JWKS at least every 24 hours and SHOULD cache it with a TTL of ≤ 1 hour.

Key rotation follows the standard JWKS rollover pattern: the issuer publishes both old and new keys for at least one cache cycle before retiring the old key. Consumers MUST verify against any key in the current JWKS.

4. Verification

A consumer verifying a Trust Envelope MUST perform, in order:

  1. Header check: alg = "EdDSA", typ = "JWT", kid present in current JWKS.
  2. Signature verification: against the public key identified by kid.
  3. Temporal check: iat ≤ now < exp. Clock skew tolerance SHOULD be ≤ 30 seconds.
  4. Issuer check: iss = "brainstormrouter" (for envelopes minted by BR).
  5. Schema check: the payload conforms to the structure in §2.

If any check fails, the consumer MUST reject the request. Logging "verified but treating as unverified" is non-conforming — there is no partial trust.

5. Lifetime and replay

exp - iat SHOULD be ≤ 300 seconds. Consumers MUST reject envelopes whose declared lifetime exceeds this bound, EVEN IF the envelope is still within its own exp.

Consumers SHOULD maintain a jti cache with TTL ≥ envelope max lifetime to reject replays. The cache MAY be in-process; a distributed cache is required only for multi-instance consumers that share state.

6. Versioning

This specification follows semantic versioning:

  • Major (v2, v3, …) — breaking claim renames, format changes, or removal
  • Minor (v1.1, v1.2) — new optional claims (no required claims added)
  • Patch (v1.0.1) — clarifications, typo fixes, additional test vectors

New required top-level claims trigger a major version bump. New optional claims may be added in a minor version; consumers MUST tolerate unknown claims without error.

The current version is v1.0.0-draft-1. v1.0.0 final ships once:

  1. At least two independent implementations pass the conformance vectors
  2. One third-party agent runtime has validated against the spec

7. Reference implementations

LanguageRepositoryLicense
TypeScript@brainstormrouter/trust-envelope on npmMIT
Pythonbrainstormrouter-trust-envelope on PyPIMIT
Gogithub.com/brainstormrouter/trust-envelope-goMIT

Each reference implementation MUST pass the conformance test vectors at specs/trust-envelope-v1-test-vectors.json (forthcoming).

The canonical TypeScript schema (Zod) is at src/security/trust-envelope/schema.ts in the BrainstormRouter source tree. Reference implementations in other languages MUST produce envelopes that round-trip through this schema.

8. Security considerations

  • Token theft: an envelope is a bearer token for its 5-minute lifetime. Transport MUST be TLS 1.3+. Consumers SHOULD bind envelopes to a session fingerprint (mTLS cert hash, client IP / ASN, or similar) and reject mismatches.
  • Budget inflation: a malicious issuer could mint envelopes with inflated br_budget.cap_usd. Consumers MUST treat the envelope as advisory and verify spend against an authoritative ledger (e.g., a Redis atomic reserve) before charging.
  • Tier downgrade attacks: an attacker who compromises the issuer's signing key could issue envelopes with elevated br_trust.tier. Mitigation: short key lifetimes + hardware-backed key storage (HSM/KMS) + rapid rotation on compromise.
  • Anomaly score gaming: anomaly_score is issuer-controlled. Consumers SHOULD treat it as one input among several, not the sole decision input.
  • Sandbox contamination: envelopes with br_test.tier: "sandbox" MUST be filtered from production metrics. Failing to filter creates a contamination class where sandbox traffic skews bandits, budgets, and dashboards.

9. References

Changelog

  • 2026-05-16 — Draft 1 published. Defines the 6-claim payload structure (br_principal, br_budget, br_scope, br_trust, br_observability, br_test), the 5-tier reputation enum, and the deny-by-default semantics for empty-array scope fields.