CAIN-42 CAIN Studio

Developer documentation

Decision record format

Last reviewed 31 August 2026

All docs

Decision record format (open specification)#

Status: version 1 of the signature domain, record schema version 3. Published so that anyone can check a CAIN decision record without trusting CAIN, and so that other tools can produce and verify the same format. This page specifies the record, the digest and the signature. It does not describe how CAIN reaches a decision.

Why this exists#

A security dashboard asks you to trust what it shows. An auditor, an insurer or your own incident review needs more than that: proof that a record says today what it said when the action was decided. Every decision CAIN records is hashed and signed with Ed25519 when it is written, and the public key is published. With the three rules below, a record exported today can be checked years later, offline, by someone with no CAIN account and no CAIN code.

1. The record#

Export one decision exactly as stored:

curl -s https://cainstudio.online/fabric/decisions/<decision_id>/signed-record \
  -H "X-API-Key: $CAIN_API_KEY" > record.json

The fields that the digest covers, in digest order:

#FieldType in the recordRule for the digest
1decision_idstringas is
2tenantstringas is
3principal_idstring or nullnull becomes the empty string
4agent_idstring or nullnull becomes the empty string
5servicestring or nullnull becomes the empty string
6pathstring or nullnull becomes the empty string
7verdictstringas is: ALLOWED, ALLOWED_DEGRADED, ALLOWED_WITH_DENIALS, REQUIRE_APPROVAL or BLOCKED
8blocked0/1 or boolean"1" if true, else "0"
9enforcing0/1 or boolean"1" if true, else "0"
10stagesstringthe stored JSON text byte for byte. Do not parse and re-serialise it
11created_atstringas is (ISO 8601, UTC)
12chain_idstring or nullnull becomes the empty string
13parent_decision_idstring or nullnull becomes the empty string
14schema versionintegerthe decimal string "3"
15outcomestring or nullnull becomes the empty string

Three more fields carry the proof: digest (hex), record_key_id and record_signature (base64). schema_version must be 3 for this specification. Other fields in the export (state, halted, chain_depth) are informational and not covered.

2. The digest#

Join the 15 values above with the unit separator 0x1F, encode as UTF-8, and take SHA-256 in lowercase hex:

import hashlib

def digest(r):
    parts = [r["decision_id"], r["tenant"], r.get("principal_id") or "", r.get("agent_id") or "",
             r.get("service") or "", r.get("path") or "", r["verdict"],
             "1" if r["blocked"] else "0", "1" if r["enforcing"] else "0",
             r["stages"], r["created_at"], r.get("chain_id") or "", r.get("parent_decision_id") or "",
             "3", r.get("outcome") or ""]
    return hashlib.sha256("\x1f".join(parts).encode("utf-8")).hexdigest()

The recomputed digest must equal the record's digest.

3. The signature#

The signed message is the domain string, 0x1F, then the digest in hex, as UTF-8:

cain.fabric.decision-record.v1 0x1F <digest hex>
  • Algorithm: Ed25519 (RFC 8032).
  • Public key: GET /fabric/decision-signing-key on any of the three sites. No account is needed,

and the response is readable from any origin.

  • Key id: the first 16 hex characters of SHA-256 over the raw 32-byte public key. It must equal the

record's record_key_id.

Fetch the key from a different site than the one that served the record. Serving a forged record together with a matching forged key would then require controlling both sites.

curl -s https://mcpgate.online/fabric/decision-signing-key

4. Verify with the reference verifier#

The reference verifier is one file. It imports no CAIN code and needs only Python 3.8+ and cryptography:

curl -sO https://cainstudio.online/proof/bundle/decision-signing-2026-09-27/verify_decision_record.py.txt
mv verify_decision_record.py.txt verify_decision_record.py
python3 verify_decision_record.py record.json --key https://mcpgate.online/fabric/decision-signing-key --self-test

It reports three checks (DIGEST, KEY, SIGNATURE). With --self-test it also runs two negative controls that must fail: 1. The verdict is changed on a copy of the record: the digest check fails. 2. The verdict is changed and the digest is recomputed, which is what someone who can write the database but does not hold the key would do: the signature check fails.

5. Test vector#

A real record from the hosted service, with its key, is published at /proof/bundle/decision-signing-2026-09-27/ (record.json, signing-key.json):

decision_id  fd_b69b43052ff44051827576f7
verdict      REQUIRE_APPROVAL
digest       e097129857d959b4a2a0d4b6314f3a0a1f45d3aa983f7b17654fa6c7bd0ef75c
key_id       8fcdf85b4a675f0e
public key   FkHArqH0TyLOOqTFtDaJqUUWj8XhXF4EGmxkaaqoWFA=  (base64, Ed25519)

An implementation of this specification is correct when it reproduces that digest and verifies that signature, and when it rejects both negative controls above.

6. What a valid record proves, and what it does not#

Proves: the record is byte-for-byte what was written at created_at by a holder of the signing key. Any later change to a covered field is detected, including a change made by someone who can write the database and recompute the digest.

Does not prove:

  • That the decision was *correct*. The record shows what was decided, not whether it should have been.
  • Anything against the operator of the gateway host. Root on that host holds both the key and the

database, so this is not third-party notarisation.

  • That a record exists. A deleted record leaves nothing to verify. Completeness needs an append-only

external log, which is not part of version 1.

For decisions ordered by the consensus cluster, GET /fabric/decisions/<id>/integrity also reports a consensus_anchor: a quorum-signed commitment to the record, checked against signatures from the cluster's replicas rather than this one key.

7. Versioning#

The signature domain (cain.fabric.decision-record.v1) and the schema version change together whenever the covered fields change. A verifier must refuse versions it does not implement rather than guess. Records written under an earlier version stay verifiable under the rules of that version.

Feedback and independent implementations: security@cainstudio.online.