> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mnemom.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Managed Rule Envelope Schema

> Wire format for the signed envelope the AEGIS promoter writes to two independent storage tiers and the gateway reads at request time. Documents the customer-readable surface; per-recipe internal columns are out of scope.

Normative reference for the **Managed Rule envelope** — the signed wire format the [AEGIS](/concepts/aegis) promoter writes to two independent storage tiers and the gateway reads at request time. This is the wire surface the gateway consumes during failover; it is not a customer-side verification surface (see §4 below).

Both envelopes — the primary-tier variant and the secondary-tier variant — share the same shape; only the signing key (`key_id`) differs.

## 1. Top-level structure

```jsonc theme={null}
{
  "recipes": [ /* array of recipe row objects, see §3 */ ],
  "signature": "<base64url(ed25519(<key_id>.<signed_at>.<sha256_hex(canonical_json(recipes))>))>",
  "key_id": "<kid; matches one entry in the corresponding JWKS>",
  "signed_at": "<ISO-8601 UTC>"
}
```

| Field | Type | Required | Notes |
| - | - | - | - |
| `recipes` | array | Yes | Array of recipe row objects. See §3. |
| `signature` | string | Yes | Base64url-encoded Ed25519 signature over the canonical-hash message. |
| `key_id` | string | Yes | Identifier of the signing key; matches the rotation cohort. |
| `signed_at` | string | Yes | ISO-8601 UTC timestamp at which the envelope was signed. |

### Canonical-hash message

The signature is computed over the UTF-8 bytes of the dotted message:

```text theme={null}
<key_id>.<signed_at>.<sha256-hex(canonical_json(recipes))>
```

where `canonical_json` recursively sorts object keys and emits compact JSON (no whitespace, no insignificant separators). The resulting digest is the SHA-256 hash in lowercase hex.

The signature wraps `key_id` and `signed_at` *in the message*, so an attacker that obtained the per-row recipe payload but did not produce the signature cannot replay the envelope under a different `signed_at`.

## 2. The two envelope variants

The promoter writes two parallel envelopes in the same canonical recipe set. They differ only in the key they use:

| Variant | Storage | Signing key | Gateway tier |
| - | - | - | - |
| **Primary envelope** | Primary storage tier | `RECIPE_KV_SIGNING_KEY` | Primary read path; no expiration — the promoter overwrites the entry on every promotion, so staleness is governed by `signed_at`, not a storage-level TTL |
| **Secondary envelope** | Secondary storage tier | `RECIPE_R2_SIGNING_KEY` | Secondary read path; per-promotion refresh |

The two signing chains are **independent**. Poisoning of the primary tier degrades gracefully to the secondary tier; the gateway's tiered read pipeline (see [Managed Rules](/concepts/managed-rules)) verifies each tier with its own JWKS binding and fails over on signature failure.

## 3. Per-recipe row shape

Each entry in `recipes` is the **full internal detection-recipe row** — including proprietary detection content (`parsed_content`, `yaml_content`) and internal routing/telemetry fields (`technique_category`, `technique_ids`, `hit_count`, `similarity_hash`, `has_tier1/2/3`, `effective_since`, `soak_ends_at`) not documented here. This page documents only the fields relevant to composition and severity semantics:

| Field | Type | Notes |
| - | - | - |
| `id` | string | Stable identifier for the recipe; survives version bumps. |
| `version` | integer | Row version. |
| `status` | enum | `"draft" \| "active" \| "inactive" \| "archived"`. The composer only ever emits `active` (and, since the observe lifecycle, `observe`) rows into the envelope. |
| `composition_scope` | enum | `"platform" \| "org" \| "team" \| "agent" \| null` (`null` treated as `platform`). Drives the [card cascade](/concepts/card-composition) target. |
| `surface` | string\[] or `null` | The screening surfaces the recipe applies to (`incoming`, `outgoing`, `tool_calls`, `tool_responses`); `null` means "applies to all surfaces". |
| `severity_p` | enum | `"p0" \| "p1" \| "p2" \| null`. See terminology note. |
| `scope` | enum | `"arena_only" \| "canary" \| "production"` — the promotion lifecycle scope (distinct from `composition_scope`). |
| `promotion_signature` | string or `null` | The per-row signature described below. |
| `dual_control_required` | boolean | Whether promotion required two-reviewer sign-off. |

Internal detection-routing fields (the detection-routing fields `door`, `threat_type`, `variant_class`, `evasion_technique`, `mitre_atlas`, and the detection content itself) are out of scope for this spec — the row carries them on the wire, but the platform writes/reads them internally; this page documents what a gateway or tooling integrator needs, not the full internal shape.

### Terminology note: `severity_p` vs "tier"

Concept-level docs (for example [Managed Rules](/concepts/managed-rules) and [AEGIS](/concepts/aegis)) use the words *tier 1*, *tier 2*, *tier 3*. The database canonical is the column `severity_p` with values `p0 / p1 / p2`. The two vocabularies refer to the same dimension; spec pages use the database canonical so wire-format consumers can map directly. The bridge is fixed:

| Concept-page label | `severity_p` value |
| - | - |
| Tier 1 (would block prod; highest severity) | `p0` |
| Tier 2 (would block prod; high severity) | `p1` |
| Tier 3 (low blast radius; observe / nudge default) | `p2` |

### Per-row promotion signature

Each recipe row also carries a per-row Ed25519 signature in its `promotion_signature` field (sourced from the internal `detection_recipes.promotion_signature` column, and — unlike the internal-only fields above — present in the envelope's `recipes[]` entries). The signature is computed over the canonical-hash inputs `recipe_id, version, composition_scope, surface, severity_p, scope, created_by, created_at` (note: `recipe_id` here names the hash-input field for this specific computation — the envelope row itself carries the same value under `id`). `created_by`/`created_at` are inputs to this signature but are not themselves present in the envelope row. The envelope signature wraps the whole recipe set and is the signature the gateway verifies at read time. The gateway does not currently re-check the per-row `promotion_signature`; it is carried for provenance.

## 4. Signature verification — internal-resilience, not customer-side

<Warning>
  At GA, the envelope signature exists for **gateway-internal verification only**. There is no public JWKS endpoint at which customers can independently verify a Managed Rule envelope signature.
</Warning>

The verification model and what it does and does not provide:

* **What the envelope signature provides.** Defense in depth between the primary and secondary read tiers. The gateway verifies on every read; a poisoned primary-tier envelope (whose signature does not verify against the primary-tier verification key set) falls through to the secondary tier (a different signing chain). A poisoned secondary-tier envelope falls through to the last-known-good in-process cache.
* **What it does not provide.** Customer-side cryptographic attestation. The verification key sets for both tiers are held server-side as secrets in the gateway's runtime environment, not customer-reachable URLs.
* **The customer-facing transparency surface.** The append-only audit chain on `recipe_review_actions` is the recipe-level provenance trail, verifiable via the admin API surface.
* **`/v1/.well-known/jwks.json` publishes a different set of keys.** That endpoint publishes [AAP](/concepts/aap-attestation) attestation keys (the `aap_signing_keys` table). Recipe-set signing keys are held in Mnemom's operator credential vault under the operator's rotation policy and are not part of the public JWKS.

A future public JWKS surface for recipe-set verification keys is not currently on the roadmap; if and when one ships, this section is updated and a customer-side verification guide lands.

## 5. Three independent signing chains

The full rule plane uses three Ed25519 keys, each rotated under Class A annual rotation (90-day JWKS overlap):

| Key | What it signs | Where it sits |
| - | - | - |
| Promotion signing key | Per-row promotion signature (`detection_recipes.promotion_signature`) | Mnemom's operator credential vault |
| Primary-tier signing key | The primary envelope (§2) | Mnemom's operator credential vault |
| Secondary-tier signing key | The secondary envelope (§2) | Mnemom's operator credential vault |

Poisoning of the primary tier degrades to the secondary tier, which uses a different signing chain. The promotion signing key signs individual rows for provenance; the gateway's read-time check is the envelope signature.

## 6. Failover behavior

The gateway's tiered read pipeline at request time:

1. **In-process memory cache** — fresh entries (\< 60 s) served from in-process state.
2. **Primary storage tier** — no expiration (overwritten on every promotion); signed with the primary-tier signing key.
3. **Secondary storage tier** — per-promotion refresh; signed with the secondary-tier signing key (independent chain).
4. **Last-known-good in-process cache** — staleness up to 24 hours.
5. **No verified rule set** — the gateway continues with an empty Managed Rules index and its built-in detection only, and raises an operator alert. Requests are not rejected.

Customers MUST NOT depend on the failover path. Under normal operation gateways pick up a changed rule set within minutes.

## 7. Failover alerting

The gateway emits internal operator alerts on the failover path, at two severities. These are operator-facing diagnostics that customers do not consume directly.

| Severity | Condition |
| - | - |
| P1 | A read against the primary tier was unreachable. |
| P0 | The primary-tier envelope signature did not verify. |
| P1 | A fallback read against the secondary tier was unreachable. |
| P0 | The secondary-tier envelope signature did not verify. |
| P1 | The last-known-good in-process cache exceeded its freshness target. |
| P0 | The last-known-good in-process cache exceeded 24 h staleness. |
| P0 | Both tiers' signatures failed in a single read — the layered-defense signal. |
| P0 | All read tiers exhausted; the gateway continues with built-in detection only. |

## 8. Example — a signed envelope at GA

The Day-1 envelope carries the five GA-seeded synthetic Managed Rules. Signatures, identifiers, and signed-at are placeholder-shaped here; production envelopes carry the real cryptographic values.

```jsonc theme={null}
{
  "recipes": [
    {
      "id": "rec_synthetic_001",
      "composition_scope": "platform",
      "surface": ["incoming"],
      "severity_p": "p2",
      "scope": "production"
    },
    {
      "id": "rec_synthetic_002",
      "composition_scope": "platform",
      "surface": ["incoming", "tool_responses"],
      "severity_p": "p2",
      "scope": "production"
    }
    /* ... three more synthetic seed entries ... */
  ],
  "signature": "<base64url ed25519>",
  "key_id": "rkv-2026-q2-a",
  "signed_at": "2026-05-30T00:00:00Z"
}
```

## 9. Validation notes

* **Versioning.** `key_id` includes the rotation cohort. The gateway verifies against its configured JWKS, never a hardcoded key.
* **Signature failure is loud.** The gateway emits `P0_kv_sig_fail` or `P0_r2_sig_fail` on signature failure and falls through to the next tier. It does not silently accept an envelope whose signature did not verify.
* **Replay defense.** The signed message includes `key_id` and `signed_at` — an envelope captured at time T cannot be replayed under a different `signed_at` because the signature would not verify.

## See also

* [Managed Rules](/concepts/managed-rules) — concept page; pipeline + reviewer modes
* [AEGIS](/concepts/aegis) — the protection-layer framing
* [Card Composition](/concepts/card-composition) — composition\_scope semantics


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.