Contents

This document specifies one Decision Evidence Object (DEO) record: envelope version e1 with payload schema v1.0. It covers the envelope and its payload, the canonical serialization, the record digest canonicalDigest, the link from a record to its predecessor, who decided (the decider), the contributions that led to the decision and the models behind them, the record's links to other records, the hashed signature expectation, including the mandate under which an agent decides, and the record's own RFC 3161 timestamp token.

Revision. This document is a Draft, and is revised in place (Specs §2, §8.4). The revision of 1 October 2026 changes the hashed field set of v1.0 (§4.1): humanAction and humanReason are renamed action and reason; the members decider, contributions and links are added; and aiBom leaves the payload, since a model's bill of materials is now recorded only in a contribution step (§7.4). A record written under the earlier field set does not verify under this revision: a verifier reports its digest as a mismatch, and names the earlier set (§4.1, Verification §4.3).

Related documents:

  • Verification: how a verifier reports on each part of a record;
  • Bundle: the proof bundle that carries a record to a verifier;
  • Anchor: the record's anchor artifact and the RFC 3161 token checks;
  • Tree: the tree construction, proofs and hash encoding;
  • Specs: requirement keywords, shared conventions, versions and the conformance vectors (Specs §3, §5).

1. Conventions

1.1 Terms

TermMeaning
recordOne DEO: a JSON object called the envelope (§2)
writerSoftware that creates a record or adds to its nonRepudiation block
verifierSoftware that checks a record against this specification
chainThe records that share one chain id, numbered by sequenceNumber from 1 (§5)
digestA record's canonicalDigest (§4)
deciderWho took the decision a record states: a human, an agent, an agent deciding under a mandate, or a policy engine (§6)
agent descriptorA document that describes an agent: its operator, identities, keys, models, instructions, tools and policy. A record names one by its digest (§6.9).
contributionWhat one contributor did toward a decision: propose, review, decide or another act. A record lists its contributions as steps (§7).
stepOne element of a record's contributions: its role, its actor, the model that drove it, what it was given and what it produced (§7)
bill of materialsThe description of the model that drove a step, by the digest of the model's descriptor (§7.4)
linkA statement in a record of how its decision relates to another record, the link's target, under a named relation (§8)
link recordA record whose links state relations between other records, made after both exist (§8.6)
mandateA grant, signed once by a principal, under which an agent decides without a signature on each record, within a stated scope and limits (§9.6)
principalThe human who signs a mandate (§9.7)
mandate statementThe statement a principal signs to issue a mandate (§9.6)
issuance recordThe record a writer makes when a mandate is issued (§9.6)

1.2 Notation

  • SHA-256 is the hash function of FIPS 180-4. ‖ joins byte strings. UTF-8(s) is the UTF-8 encoding of the string s.
  • hex(x) writes the bytes x as lowercase hexadecimal, two digits a byte.
  • B64URL(x) is the base64url encoding of x (RFC 4648 §5) with no = padding. "base64" alone is RFC 4648 §4: the standard alphabet, with padding.
  • A hash inside a JSON document follows the encoding rule of Tree §4: a string of exactly 64 hexadecimal digits. Writers emit lowercase; readers accept either case and compare the decoded 32 bytes.
  • A bare digest is a string of exactly 64 characters, each one of 0 to 9 and a to f. A prefixed digest is sha256: followed by a bare digest. Both are stricter than the hash rule: uppercase digits, surrounding whitespace or a trailing newline make the value malformed. Members that name a record or a registration use the bare form; members that name a document (a model descriptor, an agent descriptor, a pipeline definition) and every digest inside contributions use the prefixed form.
  • An integer is a JSON number read under the integer rule of Tree §4: its value is integral and lies in [0, 2^53 − 1]. 3.0 and 3e0 read as 3; a string such as "3" and a boolean are never integers. The value is the one §3.1 reads: 1.0000000000000001 reads as 1 and is an integer, and 9007199254740993 reads as 2^53 and is not. An implementation that keeps numbers as decimal text (Go's json.Number) converts each to the nearest binary64 value before it applies this rule.
  • A boolean is the JSON literal true or false, and nothing else: 1 and "true" are not.
  • Printable ASCII is the characters U+0020 to U+007E; visible ASCII is U+0021 to U+007E.
  • The length of a string is its number of UTF-16 code units, in every implementation.
  • A pattern written between slashes, as /[a-z]{3}/, matches the whole string, and its character classes are ASCII: [0-9] matches no digit outside ASCII, and no pattern accepts a trailing newline. Informative: in Python that is re.fullmatch with re.ASCII, never re.match with $.
  • A set is a JSON array of strings in strictly increasing order of their UTF-8 bytes, so it has no repeated element and exactly one spelling. A reader never sorts a set: an array out of order, or with a repeated element, is not a set. Every set this document defines holds ASCII strings, for which byte order and UTF-16 order agree.
  • A member whose value is null counts as absent, except inside the canonical serialization (§3), which writes exactly the value it is given, and inside the objects whose grammar says every member is required (§6.1, §6.9, §7.1, §7.4, §8.1, §9.2, §9.6, §9.7).
  • C(v) is the canonical serialization of the JSON value v (§3).

2. The envelope

2.1 Members

MemberTypeContent
envelopeVersionstring"e1"
payloadobjectThe decision and its context (§2.2)
integrityobjectThe digest and the chain link (§2.3)
nonRepudiationobject, optionalSignatures, timestamps and the anchor (§2.4)
custodyChainarray, optionalCustody information. This version of the specification does not define its content, and does not verify it (Specs §4).
attestationStatusstring, optionalA writer's summary of the record's attestations. It is never an input to verification.

The digest covers the hashed members of the payload (§4.1) and nothing else. Every other part of the envelope can change without changing the digest, so each part is trusted only as far as the check that reads it (Verification). A verifier ignores envelope members that this document does not name.

2.2 The payload

MemberHashedTypeContent
evidenceIdyesstringThe record's identifier, a UUID assigned by the writer
schemaVersionnostring"v1.0". It selects the hashed field set and the serialization rules (§4.3).
aiSystemIdyesstringThe AI system, or the process, whose output the decision concerns
aiOutputyesany JSON valueThe output as presented to the decider
actionyesstringThe decision taken, whoever took it, such as approve
reasonyesstringThe reason stated for the decision, whoever stated it
capturedAtyesstringThe instant of capture, an RFC 3339 timestamp in UTC
sequenceNumberyesinteger, at least 1The record's position in its chain (§5)
decideryesobject or nullWho decided (§6)
contributionsyesobject or nullThe steps that led to the decision, and the model behind each (§7)
linksyesarray or nullThe record's relations to other records (§8)
signoffExpectationyesnull or objectThe signature the record owes, or the mandate it was decided under (§9)

The types are obligations on writers. The digest covers the values as they stand, and a verifier checks a member's type only where a rule of this specification reads that member.

A writer writes no payload member other than the twelve of this table. A verifier hashes only the members of §4.1, so a further member changes no digest, and nothing vouches for its content. A verifier therefore never passes one over: it reports each further member (Verification §4.7). In particular aiBom, humanAction and humanReason, which an earlier draft hashed (§4.1), are not hashed when a payload carries them.

Writer rules. A writer writes every member of the table, with null where a member does not apply, and refuses to create a record that breaks these rules:

  • aiSystemId is a non-empty string. The values tzun:mandate and tzun:link name the records a writer makes about mandates (§9.6) and the link records it makes (§8.6), and every value that begins with tzun: is reserved: a writer MUST NOT take such a value from its caller.
  • aiOutput is a JSON object.
  • action is 1 to 128 characters of printable ASCII, with no space at its start or its end.
  • reason is 1 to 16,384 code units long and holds no lone surrogate.
  • decider is not null (§6.7).
  • a decision on the output of a model records the model in a step of contributions (§7.3).
  • links is null when the record has no link, never an empty array (§8.2).
  • the size limits of §11 hold.

2.3 The integrity block

MemberTypeContent
canonicalDigesthashThe record digest (§4.2)
chainLinkhash, or absentThe link to the predecessor (§5). Absent on a chain's first record.

2.4 The nonRepudiation block

No member of nonRepudiation is hashed. A writer MAY add a member after capture, and doing so never changes the digest; for the same reason no member is evidence on its own word.

MemberContentSpecified in
rfc3161TokenThe record's own RFC 3161 timestamp token§10
rfc3161MetadataA writer's copy of some of that token's fields, for display. Never an input to verification.—
webauthnAssertionA WebAuthn assertion made by a human signer. This version reads only its clientDataJSON.§9.3
signoffA sign-off block§9.3
assertionTimestampA timestamp over the assertion. Not verified by this version.Verification §4.7
mandateThe mandate block of a record decided under a mandate: the mandate document, the principal's statement and assertion§9.6
mandateIssuanceThe same material, carried by the record that issues a mandate§9.6
confirmationA human's WebAuthn assertion confirming a record decided under a mandate§9.6
anchorThe record's anchor artifactAnchor §8
blockchainAnchorA legacy anchor form, which verifiers abstain onAnchor §10.3

The names agentSignature (an agent's signature over its decision), contributionSignatures (signatures over individual steps of §7) and runtimeQuote (an attestation of the environment an agent ran in) are reserved for later versions. A member this table does not name, and every reserved member, is not verified by this version (Verification §4.7).

3. Canonical serialization

The canonical serialization C turns a JSON value into bytes, the same bytes in every conforming implementation. The record digest (§4), the agent descriptor digest (§6.9), the output and step digests (§7.2), the statement digest (§9.4), the mandate digest (§9.7) and the verification report (Verification §6.5) all use it.

3.1 Reading JSON

C operates on JSON values as ECMAScript's JSON.parse produces them. Every reader of a record, a bundle or a report MUST apply these rules:

  • The text is JSON (RFC 8259) in UTF-8. Bytes that are not valid UTF-8, a byte order mark, and anything after the value other than JSON whitespace make the text unreadable.
  • A number reads as the IEEE 754 binary64 value nearest to its literal, ties to even. A literal too large in magnitude reads as an infinity, which C refuses (§3.6).
  • A string, and a member name, is a sequence of UTF-16 code units. An escape of a surrogate that has no partner ("\ud800" alone) yields that lone code unit. It is kept, never replaced by U+FFFD, so that C can refuse it (§3.6).
  • When an object repeats a member name, the value of its last occurrence is the member's value.
  • A reader MUST accept arrays and objects nested 4096 deep, and MUST treat deeper nesting as unreadable.

A verifier computes every digest from the JSON it reads, by these rules, and never from a typed model of the record re-serialized: a model that fills in a missing member, or turns "3" into 3, changes the bytes.

3.2 Output

C(v) is UTF-8 with no whitespace outside strings:

  • null, true and false as those literals;
  • a number by §3.5;
  • a string by §3.4;
  • an array as [, its elements' serializations in array order joined by ,, then ];
  • an object as {, its members in the order of §3.3, each written as its name (by §3.4), : and its value, joined by ,, then }.

3.3 Member order

An object's members are sorted by name, comparing the names as sequences of UTF-16 code units: at the first position where two names differ, the smaller code unit sorts first, and a name that is a prefix of another sorts before it. A character above U+FFFF is two code units, the first in the range 0xD800 to 0xDBFF, so U+1F600 sorts before U+FF01: this order differs from sorting by code point.

The order applies at every depth, including inside arrays. Array elements are never reordered.

3.4 Strings

A string is written as ", its characters, and ". These are escaped:

CharacterWritten as
" (U+0022)\"
\ (U+005C)\\
U+0008\b
U+000C\f
U+000A\n
U+000D\r
U+0009\t
any other character below U+0020\u00 and two lowercase hex digits

Every other character is written as its UTF-8 bytes: no \u escape, no escaping of /, and no Unicode normalization.

3.5 Numbers

A number is written as ECMAScript's Number::toString writes it in radix 10, which is also how JSON.stringify writes a finite number. For a finite value x:

  1. If x is zero, of either sign, write 0.
  2. If x is negative, write - and then −x by these steps.
  3. Otherwise find integers n, k and s such that s is written with exactly k decimal digits, the first of them not zero, s × 10^(n−k) equals x, and k is the least for which such an s exists. If several values of s qualify, take the one that makes s × 10^(n−k) nearest to x, and between two equally near the even one. Write the digits of s as follows:
    • k ≤ n ≤ 21: the digits, then n − k zeros (1000000, 100000000000000000000);
    • 0 < n < k: the first n digits, ., the other k − n digits (12.5);
    • −6 < n ≤ 0: 0., then −n zeros, then the digits (0.5, 0.001);
    • otherwise, exponent form: the first digit; if k > 1, . and the other digits; then e, + if n − 1 is positive or - if it is negative, and |n − 1| in decimal (1e+21, 1e-7, 1.5e-7).

Integers therefore carry no fraction or exponent up to 10^21, and 9007199254740991 is written as is.

3.6 Refusals

C has no output, and its caller MUST treat the value as unserializable, when the value contains:

  • a lone surrogate code unit, in any string or member name at any depth;
  • a number that is not finite;
  • an array or object nested more than 64 deep, counting the value passed to C as depth 1.

A writer MUST NOT create a record whose hashed object (§4.2) is refused.

4. The record digest

4.1 The hashed field set

The hashed field set of v1.0 is exactly these eleven names:

action  aiOutput  aiSystemId  capturedAt  contributions  decider  evidenceId  links  reason  sequenceNumber  signoffExpectation

Earlier field sets. Writers that followed earlier drafts of this document hashed other sets under the same schemaVersion. Two of them are named here, so that a verifier can say why a stored digest does not recompute (Verification §4.3). A verifier never verifies a record under either. Each set is named after the writer releases that used it. The set 0.7:

aiBom  aiOutput  aiSystemId  capturedAt  evidenceId  humanAction  humanReason  sequenceNumber  signoffExpectation

and the set before-0.7:

aiBom  aiOutput  aiSystemId  capturedAt  evidenceId  humanAction  humanReason  sequenceNumber

4.2 canonicalDigest

The hashed object H of a payload is an object with exactly the members of the hashed field set of §4.1. Each member's value is the payload's member of that name, or null when the payload has no such member. Then:

canonicalDigest = hex( SHA-256( C(H) ) )

A writer stores it in integrity.canonicalDigest as 64 lowercase hex digits. It follows that:

  • a hashed member written null and one left out give the same digest; inside a hashed member, the two differ, which is why the members of §6, §7, §8 and §9.2 require every key;
  • schemaVersion, and any payload member outside the set, never affect the digest;
  • a change to any hashed value, at any depth, changes the digest;
  • a digest computed over any other set of names does not recompute under the set of §4.1. A verifier reports it as a digest mismatch, never as an older form. Naming the earlier set it recomputes under explains the mismatch and never excuses it: anyone able to rewrite a stored record can also rewrite it into an earlier set and recompute its digest (Verification §4.3).

Since the digest is the SHA-256 of C(H), a timestamp or anchor over the digest is a commitment to the canonical bytes.

4.3 Versions

envelopeVersion "e1" and schemaVersion "v1.0" are the versions this document defines. A verifier recomputes a record's digest under the hashed field set of §4.1. It MUST NOT recompute a digest for any other value of either member, an absent or non-string value included; it abstains instead (Verification §4.3). A later schema version states its own hashed field set and serialization rules.

The revision of this document noted at its head changes the set of v1.0 in place, which Specs §2 permits while the version is not frozen. Once v1.0 is frozen, a change to its set takes a new schema version.

4.4 Examples

A decision by an agent. An agent approves a payment of 1,250 on its own authority. The model that drives it proposed the payment, so the record's contributions are a list of one step, taken by the agent (§7.3). The payload

{
  "evidenceId": "5f0c1f0e-2222-4b2b-9b2b-000000000041",
  "schemaVersion": "v1.0",
  "aiSystemId": "claims-model",
  "aiOutput": {"amount": 1250, "recommendation": "pay"},
  "action": "pay",
  "reason": "Amount below the automatic-payment limit; documents complete",
  "capturedAt": "2026-09-30T14:03:11.000Z",
  "sequenceNumber": 41,
  "decider": {
    "kind": "agent",
    "id": "desc:sha256:eed92bfa7577d6f66e26fed38c35ebbec3b663a25fd6cc4c6af7d8c7475a250f",
    "agent": "sha256:eed92bfa7577d6f66e26fed38c35ebbec3b663a25fd6cc4c6af7d8c7475a250f",
    "credential": "e0d80c85a2b54cb68567fc4f7a56f20f321839af3d8f888df8b90b0a6920b386",
    "freshness": "eip155:1@9123456:0x8ae3e2d5ddeee120de1e2e58f78ed0ed9c7b7ead28098938757d597d09c3f64c"
  },
  "contributions": {
    "pipeline": null,
    "externals": [],
    "steps": [
      {
        "role": "propose",
        "actor": {
          "kind": "agent",
          "id": "desc:sha256:eed92bfa7577d6f66e26fed38c35ebbec3b663a25fd6cc4c6af7d8c7475a250f",
          "agent": "sha256:eed92bfa7577d6f66e26fed38c35ebbec3b663a25fd6cc4c6af7d8c7475a250f"
        },
        "aiBom": {
          "modelDescriptorDigest": "sha256:6c9556bc8e35e7701b44ecee10c605a4db9fdf082e6856681db7d7dd1ca3a947",
          "vendorFingerprint": null,
          "promptTemplate": null,
          "inferenceParameters": null,
          "infrastructure": null
        },
        "inputs": [],
        "output": "sha256:35f9924bbc25e4d34c48fbfd5a40e75c12048ecb92568b3e4f4be0e3510066e8",
        "verdict": null,
        "reason": null,
        "prev": null
      }
    ]
  },
  "links": null,
  "signoffExpectation": null
}

has a hashed object of eleven members, whose canonical serialization is the single line of 1,297 bytes

{"action":"pay","aiOutput":{"amount":1250,"recommendation":"pay"},"aiSystemId":"claims-model","capturedAt":"2026-09-30T14:03:11.000Z","contributions":{"externals":[],"pipeline":null,"steps":[{"actor":{"agent":"sha256:eed92bfa7577d6f66e26fed38c35ebbec3b663a25fd6cc4c6af7d8c7475a250f","id":"desc:sha256:eed92bfa7577d6f66e26fed38c35ebbec3b663a25fd6cc4c6af7d8c7475a250f","kind":"agent"},"aiBom":{"inferenceParameters":null,"infrastructure":null,"modelDescriptorDigest":"sha256:6c9556bc8e35e7701b44ecee10c605a4db9fdf082e6856681db7d7dd1ca3a947","promptTemplate":null,"vendorFingerprint":null},"inputs":[],"output":"sha256:35f9924bbc25e4d34c48fbfd5a40e75c12048ecb92568b3e4f4be0e3510066e8","prev":null,"reason":null,"role":"propose","verdict":null}]},"decider":{"agent":"sha256:eed92bfa7577d6f66e26fed38c35ebbec3b663a25fd6cc4c6af7d8c7475a250f","credential":"e0d80c85a2b54cb68567fc4f7a56f20f321839af3d8f888df8b90b0a6920b386","freshness":"eip155:1@9123456:0x8ae3e2d5ddeee120de1e2e58f78ed0ed9c7b7ead28098938757d597d09c3f64c","id":"desc:sha256:eed92bfa7577d6f66e26fed38c35ebbec3b663a25fd6cc4c6af7d8c7475a250f","kind":"agent"},"evidenceId":"5f0c1f0e-2222-4b2b-9b2b-000000000041","links":null,"reason":"Amount below the automatic-payment limit; documents complete","sequenceNumber":41,"signoffExpectation":null}

and the digest 898cffbb063bd7864b3f031b4d3699a70167caad753a96e5e9b5ebfd16719d4c. schemaVersion is left out, and the members of every object are sorted. The step's output is outputDigest(aiOutput) (§7.2), and the agent descriptor that agent names is the example of §6.9. The values of credential and freshness are illustrative: the block hash was not read from a chain, and the record is not meant to pass decider.freshness (Verification §4.9).

The same decision relabelled. Rewriting the stored decider so that it reads "kind": "human", "id": "acct:u-4821" and "agent": null, its other members unchanged, gives the digest 94704964e73bfc848e17dd76e253e27952124d319023b752ff1251a6534317ee over 1,163 bytes. That is not the stored digest, nor what the record's timestamp and anchor commit to: who decided cannot be changed without changing the digest.

A human decision on a model's proposal. An intake model, called directly with no agent identity, proposes a route, and a reviewer decides on it. The proposal is the record's one step, with a null actor (§7.3); the decision is the record's own, taken by its decider after the step:

{
  "evidenceId": "5f0c1f0e-3333-4c3c-9c3c-000000000006",
  "schemaVersion": "v1.0",
  "aiSystemId": "intake-model",
  "aiOutput": {"missingDocuments": 0, "route": "underwriting"},
  "action": "route",
  "reason": "Application complete; route to underwriting",
  "capturedAt": "2026-10-05T15:01:00.000Z",
  "sequenceNumber": 6,
  "decider": {"kind": "human", "id": "acct:u-4821", "agent": null, "credential": null, "freshness": null},
  "contributions": {
    "pipeline": null,
    "externals": [],
    "steps": [
      {
        "role": "propose",
        "actor": null,
        "aiBom": {
          "modelDescriptorDigest": "sha256:0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
          "vendorFingerprint": null,
          "promptTemplate": null,
          "inferenceParameters": {"temperature": 0, "topP": null, "maxTokens": 400, "seed": null, "stopSequences": null},
          "infrastructure": null
        },
        "inputs": [],
        "output": "sha256:593dc052d342dbf68351b0ba52869f9075925ad216c71625a45ee3afc5397586",
        "verdict": null,
        "reason": null,
        "prev": null
      }
    ]
  },
  "links": null,
  "signoffExpectation": null
}

Its hashed object is 907 bytes, and its digest 71bcc81edb3e12119420c8a6fc8188a9206b1afd7c6e46a09dc409c480377a83.

Several contributors, and a link. An underwriting decision in the same chain derives from the intake decision above. One model proposes, a second model reviews the proposal for fair lending, and a human decides:

{
  "evidenceId": "5f0c1f0e-3333-4c3c-9c3c-000000000007",
  "schemaVersion": "v1.0",
  "aiSystemId": "lending-model",
  "aiOutput": {"maxApprovedAmount": 25000, "recommendation": "approve", "riskScore": 31},
  "action": "approve",
  "reason": "Income verified; reviewer found no fair-lending concerns",
  "capturedAt": "2026-10-05T15:04:05.000Z",
  "sequenceNumber": 7,
  "decider": {"kind": "human", "id": "acct:u-4821", "agent": null, "credential": null, "freshness": null},
  "contributions": {
    "pipeline": null,
    "externals": [],
    "steps": [
      {
        "role": "propose:underwriting",
        "actor": {
          "kind": "agent",
          "id": "desc:sha256:a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
          "agent": "sha256:a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
        },
        "aiBom": {
          "modelDescriptorDigest": "sha256:0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
          "vendorFingerprint": null,
          "promptTemplate": null,
          "inferenceParameters": {"temperature": 0, "topP": null, "maxTokens": 800, "seed": null, "stopSequences": null},
          "infrastructure": null
        },
        "inputs": ["sha256:71bcc81edb3e12119420c8a6fc8188a9206b1afd7c6e46a09dc409c480377a83"],
        "output": "sha256:acda70afa64735b0b4889f7ca04b6b80998031b2d82696e37b97b03331869b57",
        "verdict": null,
        "reason": null,
        "prev": null
      },
      {
        "role": "review:fair-lending",
        "actor": {
          "kind": "agent",
          "id": "desc:sha256:b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
          "agent": "sha256:b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2"
        },
        "aiBom": {
          "modelDescriptorDigest": "sha256:0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
          "vendorFingerprint": null,
          "promptTemplate": null,
          "inferenceParameters": {"temperature": 0, "topP": null, "maxTokens": 800, "seed": null, "stopSequences": null},
          "infrastructure": null
        },
        "inputs": ["sha256:acda70afa64735b0b4889f7ca04b6b80998031b2d82696e37b97b03331869b57"],
        "output": "sha256:f59684773ae5ce28f65e4c311fa0052fb81e235ac322de987813ef39252d737f",
        "verdict": "approve",
        "reason": "No protected-class proxies in the factors",
        "prev": "sha256:32a1ccc01f6eb79975fef13137aca3f4984454bf779f62218b82a630c0d86a10"
      },
      {
        "role": "decide",
        "actor": {"kind": "human", "id": "acct:u-4821", "agent": null},
        "aiBom": null,
        "inputs": [
          "sha256:acda70afa64735b0b4889f7ca04b6b80998031b2d82696e37b97b03331869b57",
          "sha256:f59684773ae5ce28f65e4c311fa0052fb81e235ac322de987813ef39252d737f"
        ],
        "output": "sha256:ef9b809ada8800da4257ccfb6dacffbb62fe5d09ad8ab3adf1dbf236ffc9ad4a",
        "verdict": "approve",
        "reason": "Income verified; reviewer found no fair-lending concerns",
        "prev": "sha256:c8eb7215f2d39fa10a06375e225c0b35e9dd2dec2cac1314fa8b023396ff7d8b"
      }
    ]
  },
  "links": [
    {
      "rel": "derivedFrom",
      "target": {
        "digest": "71bcc81edb3e12119420c8a6fc8188a9206b1afd7c6e46a09dc409c480377a83",
        "evidenceId": "5f0c1f0e-3333-4c3c-9c3c-000000000006",
        "locator": {"origin": "test.lender.example/0f1e2d3c4b5a69788796a5b4c3d2e1f0/evidence/6c656e64696e67", "seq": 6}
      },
      "attrs": null
    },
    {
      "rel": "root",
      "target": {
        "digest": "71bcc81edb3e12119420c8a6fc8188a9206b1afd7c6e46a09dc409c480377a83",
        "evidenceId": "5f0c1f0e-3333-4c3c-9c3c-000000000006",
        "locator": {"origin": "test.lender.example/0f1e2d3c4b5a69788796a5b4c3d2e1f0/evidence/6c656e64696e67", "seq": 6}
      },
      "attrs": null
    }
  ],
  "signoffExpectation": {"policyDigest": "c6f7fb5297c99674b01b72497dafdd6cdbae77553de269ff81dc1adff5873e3a"}
}

Its hashed object is 3,165 bytes, and its digest 6fb46fa4724f9e1689a2a18fe465e8aedeaee6a5742878983a181084602b0e2e. The descriptor digests of the two agents and two models are illustrative. What the values are:

  • the proposal's output is outputDigest(aiOutput); the decision's output is outputDigest({ "action": "approve", "reason": <the record's reason> }) (§7.3);
  • the step digests are sha256:32a1ccc01f6eb79975fef13137aca3f4984454bf779f62218b82a630c0d86a10, sha256:c8eb7215f2d39fa10a06375e225c0b35e9dd2dec2cac1314fa8b023396ff7d8b and sha256:a355a0cefe7b2020f5ff81eec4aef5a51349260813791f84b62935e4053485fd, and each step's prev is the digest of the step before it;
  • the proposal's input is the intake record, named by its digest: it is an input the record declares, since a link names that record (§7.5);
  • the intake record has no lineage link of its own, so it is the root of the matter, and the root link names it too (§8.5).

Setting maxApprovedAmount to 45000 after the review gives the digest b20b48862767f129c6120eccc241cd1520edf40480eece96b75fb41569d019b5. A writer that stored the changed record with that digest would still be caught by the record's own bytes: outputDigest(aiOutput) becomes sha256:137c3b2a21a338f02db7041a8c7409f8b70bb38bafb56ee7c7326d3063aadc9e, which is not the output of the proposal that was reviewed (§7.5).

5. The chain link

The records of one chain carry sequenceNumber 1, 2, 3 and so on. The chain id is not a payload member; a bundle names it (Bundle §5.3), and so does the evidence-log origin of the record's anchor (Anchor §4.3).

  • A record with sequenceNumber 1 has no chainLink.
  • A record with sequenceNumber n > 1 carries the link to its predecessor, the record of the same chain with sequenceNumber n − 1:
chainLink = hex( SHA-256( UTF-8( Dp ‖ Dn ) ) )

where Dp is the predecessor's canonicalDigest and Dn the record's own, each written as its 64 lowercase hex digits. The preimage is the 128 ASCII characters of the two digests as text, not their decoded bytes.

The link joins the digests the two records store. A verifier decodes each stored digest under the hash rule (§1.2), re-encodes it in lowercase, and compares the link it computes with the stored chainLink case-insensitively. Whether a stored digest matches its payload is the digest check's question (Verification §4.3), not the link's.

The chain id is not hashed, on purpose. Which chain a record belongs to is bound outside the payload: by the store that holds the record, by step 2b of Anchor §10.3, which checks the chain of the record's evidence log when the verifier is given the chain id, and, for a record that another record's link names with a locator, by the locator that names its log (§8.2). A record that has been moved to another chain therefore keeps its digest, and the move is a finding of those checks, not of the digest.

6. The decider

6.1 Members

payload.decider says who took the decision the record states. It is null or an object with exactly these five members, every one of them present:

"decider": {
  "kind":       "human" | "agent" | "agent-mandated" | "policy-engine",
  "id":         "<scheme>:<value>",
  "agent":      "sha256:<64 lowercase hex>" | null,   // the agent descriptor's digest (§6.9)
  "credential": "<64 lowercase hex>" | null,          // the digest of the decider's credential registration
  "freshness":  "<CAIP-2 chain>@<block number>:0x<64 lowercase hex>" | null
}
KindWho decided
humanA person
agentAn automated agent, on its own authority
agent-mandatedAn automated agent, under a mandate a principal signed (§9.6)
policy-engineA rule engine whose decision involves no model

6.2 Grammar

The member is well formed when it is an object with exactly the five members of §6.1, and:

MemberRule
kindA string. A writer writes one of the four kinds of §6.1. A reader that meets another string abstains on the decider rather than calling it malformed (Verification §4.9), so the vocabulary can grow without a change to the grammar.
id3 to 1,024 characters of visible ASCII. The scheme is the part before the first : and matches /[a-z][a-z0-9+.-]{0,15}/; the value is the rest, and is not empty.
agentnull, or a prefixed digest (§1.2).
credentialnull, or a bare digest (§1.2). This version does not read the registration it names.
freshnessnull, or a finalized-block reference: a string matching /[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}@(0|[1-9][0-9]{0,15}):0x[0-9a-f]{64}/, whose block number is at most 2^53 − 1. Before the @ is a CAIP-2 chain; when its namespace is eip155, the reference after the : matches /[1-9][0-9]{0,30}/, so that one chain has one spelling. After the @ come the number of a block the writer read as finalized and, after the :, that block's hash.

6.3 Identifier schemes

SchemeValueWriter rule
acctThe relying party's own opaque account identifier for the person, as in acct:u-4821The value matches /[A-Za-z0-9._-]{1,128}/. It has no @, so an email address is never an identifier.
spiffeA SPIFFE ID of a workload; the whole identifier reads spiffe://<trust domain>/<path>The whole identifier matches /spiffe:\/\/[a-z0-9._-]+(\/[A-Za-z0-9._-]+)+/, and no path segment is . or ..
jktThe RFC 7638 thumbprint of the decider's public keyExactly 43 characters of base64url
descThe agent descriptor's prefixed digestEqual to agent
cmtA commitment to an identifier of another schemeA bare digest

The rules of the last column bind writers. A reader judges an identifier by the grammar of §6.2 and the consistency of §6.4 alone, the equality of a desc value with agent included, so that every reader of one record reads its decider the same way.

Commitments. A cmt identifier hides another identifier until its holder opens it:

cmt value = hex( SHA-256( UTF-8("tzun-decider-cmt-v1") ‖ salt ‖ UTF-8(inner) ) )

where salt is 32 bytes and inner is an identifier of a scheme other than cmt, written whole (acct:u-4821). The pair of the salt and inner is the opening. Nobody without the opening can predict the salt: a writer draws it from a cryptographically secure random source, or derives it with a keyed function from a secret it keeps, so that a capture retried under the same idempotency key commits with the same salt. Informative: a derived salt can be HMAC-SHA256(secret, "tzun-cmt-salt-v1" ‖ len ‖ UTF-8(key) ‖ len ‖ UTF-8(inner)), each len the 4-byte big-endian length of the field after it, with a secret of at least 32 bytes. The opening is kept outside the record: a writer that computes a commitment stores the opening where its access controls hold, and gives it to whoever must learn who decided. With the salt of the 32 bytes 0x00 to 0x1F and inner acct:u-4821, the identifier is cmt:68912cfb4b69d6b38fa0b954aa41794a8e1359afc929ee1147a4fc94ef4c14d9.

Unknown schemes. The set of schemes is open. A writer writes only the schemes of this table. A reader that meets another scheme abstains on the identifier (Verification §4.9).

6.4 Consistency

The kind, the agent reference and the scheme must agree:

kindagentSchemes permitted
humannullacct, cmt
agent, agent-mandatednot nullspiffe, jkt, desc, cmt
policy-enginenot nullspiffe, jkt, desc, cmt

A desc value equals agent. A policy engine's descriptor names no model (§6.9). The agent column is judged for every kind of §6.1, whatever the scheme; the scheme column is judged only for the schemes of §6.3. An identifier of another scheme is therefore never inconsistent by its scheme, and is left to the abstention of §6.3; with the wrong agent for its kind, it is inconsistent all the same.

For an agent kind, a writer that is given no identifier writes desc: followed by agent.

6.5 Agreement with the signature expectation and with the material

The expectation. The kind and the form of the signature expectation (§9.1) must agree, which can be decided from the record alone, with no key:

Stated kindExpectation null, or the policy formThe session formThe mandate form
humanagreesagreescontradicted
agent, policy-engineagreescontradictedcontradicted
agent-mandatedcontradictedcontradictedagrees

The material. Human decision material is a per-record assertion or a batch sign-off that binds the record (§9.5): a human signature over this record. A writer MUST NOT state a kind that the material it holds contradicts:

  • it MUST NOT write an agent kind (agent, agent-mandated, policy-engine) on a record that carries human decision material;
  • it MUST NOT write an acct decider whose value differs from the user id under which the credential that made the record's assertion is registered.

A verifier of this version verifies no signature, so it cannot tell whether bound material of the other class is genuine. When an agent kind carries bound human decision material, it reports the contradiction as undetermined, never as a pass (Verification §4.9).

6.6 Freshness

A finalized-block reference shows that the decision was captured no earlier than the block it names, since nobody can name a block's hash before the block exists. A writer MUST take the reference from a block it read as finalized on the chain it names, and MUST NOT name a block later than one it has read. A writer that has no such reference, or only a stale one, writes null.

The chain is the writer's choice, in practice the chain it anchors on, which is a matter of its configuration (Anchor §7.2). The registry of Anchor §9 names Ethereum mainnet (eip155:1) and the Sepolia (eip155:11155111) and Hoodi (eip155:560048) test networks; the grammar of §6.2 admits any chain.

A verifier checks the reference against a header source of its own (Anchor §10.4): that the block named is the canonical block at that height (Verification §4.9). The difference between capturedAt and the block's time is informative.

6.7 Decider not recorded

null means that the decider is not recorded. A verifier reports it as such and never infers a decider, from a signature, from custody information or from any other part of the record. A writer writes a decider on every record; a null decider comes from a writer that does not follow this rule, and the grammar admits it so that a later version can admit such writers without a change to the grammar.

6.8 What a decider discloses

A decider identifier sits in a record that is anchored and cannot be changed, and so cannot be erased. An opaque account identifier limits what it reveals, but it still identifies a person to whoever can map it. A writer MUST write opaque identifiers, never an email address or a person's name, in every identifier it puts into a record or its signed material: a decider's or an actor's id, a mandate's principal and signer (§9.6, §9.7), and the attributes of a link (§8.9). A chain id, which a link's locator discloses (§8.9), follows the same rule. A writer SHOULD use cmt for any record that leaves the organization that made it.

6.9 Agent descriptors

An agent descriptor describes one agent as it is configured at a moment. A record names it by its digest, in the agent member of a decider (§6.1) or of a step's actor (§7.1), and so commits to everything the descriptor commits to. The descriptor is an object with exactly these members, every one of them present, at every depth:

{
  "v":            "tzun-agent/1",
  "name":         "<string>",                                   // the agent's name, given by its operator
  "operator":     { "organization": "<string>", "owner": "<64 lowercase hex>" | null },
  "identities":   ["<string>", …],                              // workload identities, such as SPIFFE IDs
  "keys":         [ { "kid": "<RFC 7638 thumbprint>", "alg": "EdDSA" | "ES256" | "PS256",
                      "custody": "hsm" | "tee" | "workload" | "software",
                      "attestation": "<string>" | null } ],
  "models":       ["sha256:<64 hex>", …],                       // the models that drive it; [] for a policy engine
  "instructions": "sha256:<64 hex>" | null,                     // its standing instructions
  "tools":        "sha256:<64 hex>" | null,                     // its tool manifest
  "runtime":      { "format": "<string>", "measurements": ["<string>", …] } | null,
  "policy":       "sha256:<64 hex>" | null,                     // its operating policy
  "parent":       "sha256:<64 hex>" | null,                     // the descriptor it was derived from
  "derivation":   "base" | "revision" | "fork" | "clone" | null
}

Grammar. v is "tzun-agent/1". name, operator.organization and runtime.format are non-empty strings. operator.owner is a bare digest, naming the registration of the person responsible for the agent, which this version does not read, or null. Each element of identities and runtime.measurements, and each attestation that is not null, is a non-empty string. kid is exactly 43 characters of base64url. models holds prefixed digests of model descriptors, and instructions, tools, policy and parent are null or prefixed digests. The arrays identities, keys, models and runtime.measurements hold no element twice, two keys with one kid included; their order is free.

Digest.

agentDescriptorDigest = "sha256:" ‖ hex( SHA-256( C(d') ) )

where d' is the descriptor with identities, models and runtime.measurements each sorted by the UTF-8 bytes of their elements, and keys sorted by the UTF-8 bytes of their kid. The order in which a writer assembled an array therefore never changes the digest. A store that keeps a descriptor keeps the bytes C(d'), so that a verifier re-hashes the bytes it holds rather than a descriptor it has parsed and serialized again.

Writer limits. A writer refuses to register a descriptor whose canonical bytes exceed 16 KiB, an array of more than 64 elements, a string of more than 256 code units (an attestation of more than 8,192), or a lone surrogate. name and operator.organization name a product and an organization, never a person (§6.8).

Example. The descriptor of a claims-triage agent whose key is the Ed25519 key of RFC 8037 Appendix A, whose thumbprint is kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k, has the canonical bytes

{"derivation":"base","identities":["spiffe://claims.example/agent/triage"],"instructions":"sha256:be6682dd9f0ba8bbbb060e759803b0b71b69342105824467a8d918a191b391e0","keys":[{"alg":"EdDSA","attestation":null,"custody":"software","kid":"kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k"}],"models":["sha256:6c9556bc8e35e7701b44ecee10c605a4db9fdf082e6856681db7d7dd1ca3a947"],"name":"claims-triage-agent","operator":{"organization":"Example Insurer","owner":"399bad81f76ef3e1a781d4edcf6eee0b54ce4cfc3304ca56b50d7457910c13b9"},"parent":null,"policy":"sha256:16c9ce7658ad1a8dd72bc047ce935212fc29b829818bac33e61546c3f9c57589","runtime":null,"tools":"sha256:d2269b9f0ab7e0a1ab7bf010b7f60ff9ab2fea2e6f728b666f0bc5e5ebf51ca9","v":"tzun-agent/1"}

(729 bytes) and the digest sha256:eed92bfa7577d6f66e26fed38c35ebbec3b663a25fd6cc4c6af7d8c7475a250f, the agent of the first example of §4.4. Its component digests and its owner are illustrative.

What a verifier reads. A b1 bundle carries no descriptor, and a verifier of a bundle does not resolve the reference (Verification §4.8). A verifier that holds the descriptor's bytes, such as a store's own, re-hashes them and compares the result with the reference, and checks that a policy engine's descriptor names no model (§6.4).

7. Contributions

7.1 Shape

payload.contributions lists, in the order they acted, the contributors whose work led to the decision, and the model that drove each of them. It is null or an object with exactly these three members, every one of them present, and each step has exactly the eight members shown:

"contributions": {
  "pipeline":  "sha256:<64 hex>" | null,             // the digest of a pipeline definition
  "externals": ["sha256:<64 hex>", …],               // declared external inputs: a set of 0 to 64
  "steps": [                                         // 1 to 32, in the order the contributors acted
    {
      "role":    "<class>" | "<class>:<name>",
      "actor":   { "kind": "<kind>", "id": "<scheme>:<value>", "agent": "sha256:<64 hex>" | null } | null,
      "aiBom":   { … } | null,                       // the model that drove the step (§7.4)
      "inputs":  ["sha256:<64 hex>", …],             // everything the step was given: a set of 0 to 64
      "output":  "sha256:<64 hex>",                  // the output digest of what the step produced
      "verdict": "approve" | "reject" | "concerns" | null,
      "reason":  "<string>" | null,
      "prev":    "sha256:<64 hex>" | null            // null on step 0, the digest of the step before otherwise
    }
  ]
}
MemberRule
pipelinenull, or a prefixed digest. This version does not define pipeline definitions or read the one named.
externals, inputsSets (§1.2) of prefixed digests, each of 0 to 64 elements
roleA class, one of propose, review, decide and other, optionally followed by : and a name matching /[A-Za-z0-9._-]{1,48}/
actornull, or exactly kind, id and agent, under the grammar and consistency of §6.2 to §6.4. null is allowed only on a step whose aiBom is not null and whose class is not decide: a step has an identity or a model, and a decision has an identity.
aiBomnull, or an object. A writer writes the object of §7.4; a reader checks none of its members (§7.4).
output, prevPrefixed digests; prev may be null
verdictnull, or one of the three strings
reasonnull, or a string. A writer writes 1 to 16,384 code units (§11).

7.2 Output and step digests

outputDigest(v) = "sha256:" ‖ hex( SHA-256( C(v) ) )
stepDigest(i)   = "sha256:" ‖ hex( SHA-256( C(steps[i]) ) )

outputDigest names what a step produced, or what it was given, by the digest of its canonical bytes. A byte output, such as a file, is represented by the structured value { "mediaType": "<media type>", "sha256": "<bare digest of the bytes>" }, and its output digest is taken over that value.

Every digest inside contributions is prefixed, since inputs can mix output digests and record digests: a record that one of the record's links names (§8) appears in inputs as sha256: followed by its bare digest.

7.3 Rules

A writer refuses to create a record that breaks these rules, and a verifier reports a record that breaks one as a finding about the record's form (Verification §4.10):

  • Sets. externals and every inputs are sets. A writer that sorts them does so before it computes any digest; a reader never sorts.
  • Verdicts. verdict is null for the classes propose and other.
  • The deciding step. At most one step has the class decide, and when one does, it is the deciding step. When none does, no step is the deciding step: the record's decider took the decision after the last step. No step of the class propose or review follows a step of the class decide; a step of the class other may, such as one that carries the decision out.
  • The step chain. prev of step 0 is null, and prev of each later step i is stepDigest(i − 1). Changing, inserting, removing or reordering a step therefore changes every step after it.
  • The decider is the deciding actor. When there is a deciding step, the record's decider and the deciding step's actor have the same kind, id and agent, compared as canonical bytes. A writer that hides the decider behind a commitment writes the same cmt identifier in both.
  • The decision itself. The output of a step of the class decide is outputDigest({ "action": <action>, "reason": <reason> }), built from the record's own action and reason, with null for a member the payload lacks, as in the hashed object (§4.2).

A single model is a list of one. A step's bill of materials is the one place a record names a model, so a decision on the output of one model records one step: of the class propose, its aiBom naming the model, its output outputDigest(aiOutput), and its actor the agent that called the model, or null when the model was called directly with no agent identity. The first two examples of §4.4 show both. Every model that took part in a decision with several contributors is named by the aiBom of its own step.

7.4 The step bill of materials

A step's aiBom is null or an object with exactly these members, every one of them present:

"aiBom": {
  "modelDescriptorDigest": "sha256:<64 lowercase hex>",   // the model descriptor
  "vendorFingerprint":     "<string>" | null,
  "promptTemplate":        { "templateId": "<string>" | null, "templateDigest": "<string>" | null } | null,
  "inferenceParameters":   { "temperature": <number> | null, "topP": <number> | null,
                             "maxTokens": <number> | null, "seed": <number> | null,
                             "stopSequences": ["<string>", …] | null } | null,
  "infrastructure":        { "provider": "<string>" | null, "region": "<string>" | null,
                             "apiVersion": "<string>" | null, "endpoint": "<string>" | null } | null
}
  • A writer writes every member listed, at every depth, with null for a value it does not have. A missing member and a null one give different canonical bytes, so a writer that fills in absent members MUST do so before it computes the digest.
  • modelDescriptorDigest is a prefixed digest (§1.2) of the model's descriptor, a document this version does not define or read.
  • A verifier hashes aiBom as it stands, whatever members it has, and checks none of them. The one member a rule of this version reads is modelDescriptorDigest, when a mandate restricts the models (§9.9).

7.5 Binding

These rules read only hashed members and need no key. A record that breaks one is still evidence of what happened: a writer MAY create it, and a verifier reports it as a finding about the process the record states, apart from the rules of §7.3 (Verification §4.10).

  • Terms. P* is the last step of the class propose before the deciding step, or, when there is no deciding step, the last step of the class propose. A step of the class review covers the output of the last propose step before it; a review with no propose step before it covers none.
  • Input provenance. Each input of step i is the output of an earlier step, sha256: followed by the target.digest of one of the record's links, or an element of externals. Links declare inputs only when links meets the grammar of §8.2; links that do not declare none, so that every verifier reads the same inputs.
  • Review binding. A review that covers an output has that output among its inputs.
  • Current and stale reviews. A review that covers the output of P* is current. A review that covers an earlier proposal is stale: it is reported, and never counted as an approval of the decision.
  • Decision binding. When there is a P*, outputDigest(aiOutput) equals the output of P*, and, when there is a deciding step, its inputs include the output of P*: what was decided on is what was proposed and reviewed. With no P*, decision binding does not apply.

7.6 What output digests disclose

An output digest of a value with little entropy, such as {"verdict":"approve"} or a small aiOutput, can be found by trying the likely values. Where a step's output must not be guessable, a writer takes the output digest over { "salt": "<base64url of 16 bytes or more>", "value": … } and keeps the salt outside the record, with the value it hides.

8. Links

8.1 Shape

payload.links states how the record's decision relates to other records. It is null or an array of links, and each link is an object with exactly the three members shown, every one of them present, as are the members of target and of locator:

"links": [                                                 // 1 to 256, sorted (§8.2)
  {
    "rel":    "<relation>",                                // a registered name, or x-<org>.<name>
    "target": {
      "digest":     "<64 lowercase hex>",                  // the target record's canonicalDigest
      "evidenceId": "<string>" | null,                     // the target's evidence id
      "locator":    { "origin": "<evidence-log origin>", "seq": <integer, at least 1> } | null
    },
    "attrs":  { "<name>": <value>, … } | null              // what the relation's profile admits (§8.3)
  }
]

A link says what this record's decision takes from, or states about, the record its target names: the record that carries the link is the child, and its target the parent. The target is fixed in this record's digest, and a digest cannot be computed before the digests it covers, so the links that records carry about themselves form a directed acyclic graph. The relations that a link record states between two other records (§8.6) are assertions, and can form cycles: one link record can say that A supersedes B, and another that B supersedes A.

For every registered relation the target is a record, and target.digest is its canonicalDigest. An extension relation may name any SHA-256 digest.

8.2 Grammar

ItemRule
linksnull, or an array of 1 to 256 links. An empty array is malformed: "no link" has one spelling, null.
linkAn object with exactly rel, target and attrs.
rel, registered/[a-z][A-Za-z0-9]{0,31}/. A writer writes only the names of §8.3.
rel, extension/x-[a-z0-9]([a-z0-9-]{0,30}[a-z0-9])?\.[a-z][A-Za-z0-9]{0,31}/, at most 67 characters, as in x-acme.contractRenewal. <org> is a label its owner controls; a label of the organization's domain name is the convention. Extension names are never registered.
targetAn object with exactly digest, evidenceId and locator.
target.digestA bare digest (§1.2).
target.evidenceIdnull, or 1 to 128 characters of visible ASCII. A record of this document has a UUID; a record of another system may have another form of identifier.
target.locatornull, or an object with exactly origin and seq. seq is an integer of at least 1, the target's sequenceNumber, whose leaf index is seq − 1. origin is the origin of a log that can hold a record's leaf: a Tzun origin (Anchor §4.3.1) that is either a store's evidence log, <ns>/<storeId>/evidence/<chain segment>, whose namespace follows the grammar of ns, whose store id is 32 lowercase hex characters (Anchor §4.3.2) and whose chain segment meets the chain segment rule there; or a hosted log, <ns>/<id>/<logId>, whose namespace is exactly log.tzun.ai or exactly test.log.tzun.ai and whose last segment is not anchors (Anchor §4.3.3).
attrsnull, or an object of 1 to 16 members; an empty object is malformed. A member's name matches /[a-z][A-Za-z0-9]{0,31}/ or the extension grammar of rel. Its value is a string of 0 to 256 characters of printable ASCII, an integer (§1.2), or a boolean. No value is null, an object or an array: an attribute that does not apply is left out.
OrderThe links are in strictly increasing order of the pair (rel, target.digest): compared by the bytes of rel, a name that is a prefix of another sorting first, and then by the bytes of target.digest. No two links share the pair. A reader never sorts: links out of order, or a repeated pair, are malformed. Every character is ASCII, so byte order and UTF-16 order agree.

One target can appear under several relations: the last example of §4.4 names the intake record under derivedFrom and under root.

8.3 Relations

A relation is defined by its name, its meaning, its attribute profile, which names every attribute it admits, and its rules. A rule is of one of three kinds:

  • a list-level rule reads the whole links member together with the record's aiSystemId, action, contributions and signoffExpectation, so that it can fail when a link it requires is missing;
  • a record-local rule reads this record's own members, so that every verifier checks it;
  • a target rule reads the target record's members, so that a verifier checks it only when it holds the target (§8.8).

Each rule has a name, which a verifier reports when the rule fails (Verification §4.11).

List-level rules.

NameRule
mandate_linkExactly one mandate link when signoffExpectation is the mandate form (§9.1), and none otherwise. When the expectation is malformed (§9.2), this rule is not applied.
revokes_linkExactly one revokes link when the record is a revocation record (§9.6: aiSystemId "tzun:mandate" and action "revoke-mandate"), and none otherwise.
subject_linkExactly one subject link when aiSystemId is "tzun:link" (§8.6), and none otherwise. With the subject link, action is "link", contributions is null, the record has at least one other link, no other link has the subject link's target.digest, and no root, mandate, revokes, closes or fanoutOf link is beside it.
at_most_oneAt most one root, one closes and one fanoutOf link.

Profiles are closed. An attribute that a registered relation's profile does not name, an extension attribute included, breaks the profile, and so does a named attribute that is missing or whose value is not of its type. An extension relation admits any attributes under the grammar of §8.2.

Lineage relations are the relations that make this record a child of its target in the record's lineage: derivedFrom, reviews, supersedes, appeals, ratifies, implements, delegatedBy, closes and fanoutOf.

The registry. "This record" is the record that carries the link; "the decider" of a record is its payload.decider. Two deciders are the same when their kind and their id are equal strings.

relThis record…attrsRecord-local ruleTarget rule
derivedFromuses the target's decision, or its output, as an inputnull——
reviewsis a review of the target's decisionnull—reviews_distinct_decider: this record's decider is not the same as the target's. A verifier that can open a cmt decider (§6.3) compares the identifier it commits to. When either decider is a cmt identifier it cannot open, the rule is undetermined, unless both are the same cmt identifier.
supersedesreplaces the target's decision on the same matternull—supersedes_same_system: the target's aiSystemId is this record's.
appealsrecords an appeal against, or a contest of, the target's decisionnull——
ratifiesapproves, after the fact, a decision the target took on its own authoritynullratifies_decider_human: this record's decider has the kind human. A decider of a kind this version does not define is not human, and fails the rule.ratifies_target_not_human: the target's decider does not have the kind human.
implementscarries out the target's decisionnull——
delegatedByis taken under authority the target delegatednull—That the target carries the authority delegated. This version does not define the rule, and a verifier abstains on it.
closescloses the fan-out opened by the target (§8.5)statement, a bare digest; size, an integer; treeRoot, a bare digest; all three required—The closure of the fan-out. This version does not define the rule, and a verifier abstains on it.
fanoutOfis one decision of the fan-out opened by the target (§8.5)statement, a bare digest; slot, an integer of at least 1; both required—As for closes
rootbelongs to the matter whose first decision is the target (§8.5)nullroot_without_lineage: the record has at least one lineage link. When it has none, and another of its links has a name that this version does not register and that is not an extension name, which a later registry may define as a lineage relation (§8.4), the rule is not decided. An extension name is never registered, so a root link beside extension links alone fails the rule.None: the target conditions of §8.5 are reported as facts, never as a failure.
mandateis decided under the mandate whose issuance record is the target (§9.6)null—mandate_target: the target is an issuance record whose aiOutput.assertionDigest is this record's signoffExpectation.mandateAssertion.
revokesrevokes the mandate whose issuance record is the target (§9.6)null—revokes_target: the target is an issuance record whose aiOutput.mandateDigest is this record's aiOutput.mandateDigest.
subjectis a link record: its other links state relations of the subject, the target of this link, and not of this record (§8.6)null——

An issuance record is a record whose aiSystemId is "tzun:mandate" and whose action is "issue-mandate" (§9.6).

8.4 The registry rule

  • A relation is added by an entry in the registry of §8.3: its name, its meaning, its attribute profile and its rules. An addition changes no digest, no grammar and no schema version: a record that uses a relation registered after a verifier was built is well formed for that verifier, which abstains on the relation's rules (Verification §4.11).
  • A registered relation's profile and rules never change. A changed meaning takes a new name.
  • Extension names are never registered. Two organizations can choose the same label, so an extension relation has no meaning that every reader shares: a tool that interprets one does so for the writer it knows, and a verifier never interprets one. An organization that wants a relation checked by every verifier proposes a registered name.
  • A writer refuses a name that is neither registered nor an extension name. A reader abstains on one, since a later registry may define it.

8.5 Lineage: roots and fan-outs

The root. A record with no lineage link is a root: the first decision of its matter. It has no root link. A record with a lineage link belongs to the matter of its lineage targets, and its root link names the matter's root. A writer that holds the target of one of a record's lineage links writes the root link: it takes the first lineage link, in the order of §8.2, whose target it holds, and names that target's own root. A record's own root is the target of its root link, or the record itself when it is a root; a record that has a lineage link and no root link, such as a link record (§8.6), has none, and a writer whose first held lineage target has none writes no root link. A writer that holds no lineage target writes a root link only when the target's holder gives it the root, and a writer that cannot tell the root writes none.

The root is informative. A verifier that holds the root link's target reports the fact rootInconsistent when that target has a lineage link of its own, and a verifier that holds a lineage target that has a root reports it when that root is not the root link's target (Verification §4.11). A held lineage target that has no root gives no fact. Neither fact is a failure.

Fan-outs. A decision can be split into sub-decisions of one fan-out. A sub-decision names the record that opened the fan-out with a fanoutOf link, whose attributes give the fan-out's statement and the sub-decision's slot; the record that closes it names the same record with a closes link, whose attributes give the statement, the number of sub-decisions size and the treeRoot over them. This version defines these profiles and no rule that checks them: a writer of this version writes neither relation, and a verifier abstains on their target rules.

8.6 Link records

A relationship that becomes known after both records exist is stated by a link record: a later record whose links name both ends. Neither end changes; the link record carries its own decider, time, digest and anchor. A link record has the aiSystemId "tzun:link", the action "link", contributions null, one subject link, which names one end, and one or more links that name the other ends, none of them naming the subject's digest. Each of those links says that the subject stands in its relation to its target, as asserted by the link record's decider.

The rules of each such relation apply with the subject in place of this record: a record-local rule of the relation becomes a target rule on the subject. In a link record, ratifies therefore needs the subject's decider, not the link record's, to be human, which a verifier checks when it holds the subject. A link record carries no root, mandate, revokes, closes or fanoutOf link, since each binds the record that carries it. These conditions on a link record's form are the list-level rule subject_link (§8.3).

Example. A complaint recorded in another store, whose digest is c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0, contests the underwriting decision of §4.4. The lender records the relationship in its own chain:

{
  "evidenceId": "5f0c1f0e-3333-4c3c-9c3c-000000000019",
  "schemaVersion": "v1.0",
  "aiSystemId": "tzun:link",
  "aiOutput": {},
  "action": "link",
  "reason": "Complaint C-1182 contests the underwriting decision",
  "capturedAt": "2026-10-19T09:30:00.000Z",
  "sequenceNumber": 19,
  "decider": {"kind": "human", "id": "acct:u-7702", "agent": null, "credential": null, "freshness": null},
  "contributions": null,
  "links": [
    {
      "rel": "appeals",
      "target": {
        "digest": "6fb46fa4724f9e1689a2a18fe465e8aedeaee6a5742878983a181084602b0e2e",
        "evidenceId": "5f0c1f0e-3333-4c3c-9c3c-000000000007",
        "locator": {"origin": "test.lender.example/0f1e2d3c4b5a69788796a5b4c3d2e1f0/evidence/6c656e64696e67", "seq": 7}
      },
      "attrs": null
    },
    {
      "rel": "subject",
      "target": {"digest": "c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0", "evidenceId": null, "locator": null},
      "attrs": null
    }
  ],
  "signoffExpectation": null
}

Its hashed object is 811 bytes, and its digest 9baa0f50a76ed0280d16be30dfe51571fb37b7952c144023a87ba1f61debf454. It has no root link, although the decision it names has a root.

8.7 Writing links

A writer refuses to create a record whose links break the grammar of §8.2, a list-level rule, a profile or a record-local rule, and, for a target it holds, a target rule. In addition:

  • Targets it holds. A writer resolves a target in its own store by evidenceId when the caller gives one, and by digest otherwise, and refuses a target it does not hold. It refuses a member the caller gave that disagrees with the record it holds: a digest that is not that record's stored digest, an evidenceId that is not its evidence id, a locator.origin that is not the origin of its chain's evidence log, and a locator.seq that is not its sequenceNumber. It fills in digest, evidenceId and locator from the record it holds, except that it writes locator as null when the caller asks it to withhold the target's origin (§8.9).
  • Targets in another store. For a target the caller marks as held elsewhere, digest is required, and evidenceId and locator are written as the caller obtained them from the store that holds the target, or null. A target so marked that the writer's own store holds, by digest or by evidence id, or whose locator names one of the writer's own logs, is treated as a target it holds. Without the mark, a target the writer does not hold is refused, so that a mistyped target never becomes a link to a record that does not exist.
  • Reserved relations. A writer writes mandate, revokes and subject links only on the records of §9.6 and §8.6 that it builds itself, never as links its caller supplies. A writer of this version writes no closes or fanoutOf link (§8.5).
  • Order. A writer that sorts links does so before it computes any digest, and refuses a repeated pair rather than removing it.

8.8 What a verifier checks

A verifier checks the grammar of §8.2 and the list-level rules of §8.3 over the whole member, and, for each link of a relation it knows, the profile and the record-local rule. It checks a target rule only when it holds the target. A b1 bundle carries no target, so a verifier of a bundle holds none (Verification §4.11). A verifier that holds targets, such as a store's own, resolves a target by target.digest, which is authoritative, and then compares each member of target that is not null with the record it holds: evidenceId with its evidence id, locator.seq with its sequenceNumber, and locator.origin with the origin of its chain's evidence log. Resolving by digest first keeps verifiers in agreement: a link whose digest is one record's and whose evidence id is another's disagrees with its target in every verifier. A verifier that holds the log a locator names, and finds another record at that position, or none, also reports the locator as disagreeing with the target, whether or not it holds the target.

This version verifies no edge: it does not show that a target was on record before the record that names it, nor that it sat at the position its locator names. A verifier reports every link's edge as one it does not verify (Verification §4.11), so a record with links never passes in silence.

8.9 What links disclose

A link discloses its target's digest, and with a filled evidenceId and locator the target's evidence id and the origin of its log: the store's namespace and id and the chain id, which the chain segment spells as the hex of its UTF-8 bytes and which anyone can decode. A digest discloses nothing of the target's payload, which includes a random evidence id. A chain id therefore follows the rule of §6.8, and so does every string attribute: an attribute is opaque data in a record that cannot be erased, and never an email address or a person's name. Where a target's origin must not be disclosed, a writer writes locator as null; a later proof of the edge then needs the target's holder to disclose the locator.

9. The signature expectation and mandates

9.1 Forms

payload.signoffExpectation says which human signature the record owes, or, for a record decided by an agent under a mandate, which mandate it was decided under. It is hashed (§4.1), so the record's digest, and with it the record's timestamp, chain link and anchor, commit to the answer.

"signoffExpectation": null                                                  // no signature is owed
                    | { "policyDigest": "<64 lowercase hex>" }              // a signature is owed under this policy
                    | { "statementDigest": "<64 lowercase hex>", "sessionSeq": 7 }  // captured in a signed session
                    | { "mandateAssertion": "<64 lowercase hex>", "mandateSeq": 12,  // decided under a mandate
                        "totals": { "paid": 5785000 } | null, "confirm": false }
  • null: the record owes no signature. A writer writes null unless one of the other forms applies.
  • The policy form: the record owes a signature from a human signer, made for this record alone or for a batch of records that includes it, under the sign-off policy whose digest is policyDigest. The signature MAY be attached after capture, since nonRepudiation is not hashed. This version does not interpret the policy that policyDigest names.
  • The session form: the record is captured inside a signed session. statementDigest is the digest (§9.4) of the session's sign-off statement, and sessionSeq the record's slot in the session, counting from 1. Slots need not be contiguous. A writer MUST store a session record's signoff block (§9.3) and its assertion with the record itself, in the same write.
  • The mandate form: the record was decided by an agent under a mandate (§9.6). mandateAssertion is the assertion digest (§9.6) of the principal's assertion that issued the mandate; mandateSeq is the record's slot in the mandate, counting from 1; totals holds, for each cumulative cap of the mandate, the running total including this record, and is null when the mandate has none; confirm says whether a human confirmation of the record is owed. A writer MUST store the record's mandate block (§9.6) with the record itself, in the same write, and the record has a mandate link to the mandate's issuance record (§8.3).

9.2 Grammar

A value is well formed when it is one of:

  • null, or the member absent, which is the same value (§4.2);
  • an object with exactly one member, policyDigest;
  • an object with exactly two members, statementDigest and sessionSeq;
  • an object with exactly four members, mandateAssertion, mandateSeq, totals and confirm.

policyDigest, statementDigest and mandateAssertion are bare digests (§1.2): uppercase digits, a 0x prefix, surrounding whitespace or a trailing newline make the value malformed. sessionSeq and mandateSeq are integers (§1.2) of at least 1; a string such as "4", a boolean and a fraction are malformed. totals is null or an object of 1 to 16 members, each named by a cap name matching /[A-Za-z0-9._-]{1,64}/ and each an integer. confirm is a boolean (§1.2).

Any other value, such as a string, a number, an array, an empty object, or an object with another member or with members of two forms, is malformed.

9.3 Sign-off material

These terms describe what a record carries under nonRepudiation:

  • Assertion. webauthnAssertion when its value is a JSON object. Any other value is no assertion. This version reads one member of it, clientDataJSON: a string holding the WebAuthn client data, a JSON text, encoded in base64url or base64. The string decodes when, once every = at its end is removed, it decodes as unpadded base64url (RFC 4648 §5) or, failing that, as unpadded base64 (RFC 4648 §4), in either case skipping any CR and LF and with pad bits that need not be zero. The assertion presents the challenge c when that string decodes, the result parses (§3.1) as a JSON object, and the object's challenge member is the string c. The object's type member is the assertion's type. An assertion whose clientDataJSON is missing, does not decode or does not parse presents no challenge. The same terms apply to every WebAuthn assertion this document names: the mandate block's assertion and the confirmation (§9.6).
  • Sign-off block. The value of signoff when it is not null, of whatever JSON type. When it is an object, its members are statement, a sign-off statement (§9.4); inclusion, an object { "leafIndex": <integer>, "path": [<hash>, …] }, for a batch; and sessionSeq, an integer, for a session.
  • A record carries a sign-off when it has both a sign-off block and an assertion.
  • A session sign-off is a sign-off block that is an object and that either has a sessionSeq member or has a statement that is an object whose mode is the string "session". It is a session sign-off whether or not an assertion accompanies it.
  • Mandate block. The value of mandate when it is not null, of whatever JSON type (§9.6).
  • Issuance block. The value of mandateIssuance when it is not null, of whatever JSON type (§9.6).

9.4 Statement digest and challenges

A writer writes a sign-off statement as a flat JSON object. This version reads three of its members: mode, which is "batch" or "session", and, in a batch statement, root (a hash) and treeSize (an integer). Its other members lie outside this version of the specification; a verifier that follows it reads only the members named here. A mandate statement (§9.6) is a sign-off statement whose mode is "mandate".

A statement lies in the value domain when every member name is printable ASCII (the bytes 0x20 to 0x7E), and every member value is either a string of printable ASCII or an integer (§1.2). A nested object or array, null, a boolean, a negative or fractional number, a number above 2^53 − 1, and any other character put a statement outside the domain. Only a statement in the domain has a digest:

statementDigest(S) = hex( SHA-256( C(S) ) )

For such a statement C sorts the members by their bytes, writes integers as plain decimal, and escapes only " and \.

An assertion can present one of three challenges:

per-record challenge of a record with digest D    = B64URL( SHA-256( UTF-8("tzun-decision-v1") ‖ UTF-8(D) ) )
statement challenge of a statement S              = B64URL( SHA-256( UTF-8("tzun-signoff-v1")  ‖ UTF-8(statementDigest(S)) ) )
confirmation challenge of a record with digest D  = B64URL( SHA-256( UTF-8("tzun-confirm-v1")  ‖ UTF-8(D) ) )

D and statementDigest(S) enter as their 64 lowercase hex digits. No separator is a prefix of another, and the per-record preimage (80 bytes) differs in length from the other two (79 bytes), so no challenge of one kind is ever computed over the preimage of another. For the record decided by an agent in §4.4, the per-record challenge is ynOTh1f-lpO51l8hSQ7qq-Qq6C3v_136KurN-QjlwdI.

9.5 Binding

Binding is what can be read from a record without any key: which challenge its assertion presented, and where a batch statement puts its digest. It never establishes that a signature verifies. For a record with digest D:

  • A per-record assertion binds the record when the record has no sign-off block, and the assertion's type is "webauthn.get" and it presents the per-record challenge of D.
  • A batch sign-off binds the record when all of these hold:
    • the sign-off block and its statement are both objects, the statement lies in the value domain (§9.4), and its mode is "batch";
    • the assertion presents that statement's challenge;
    • the statement's root is a hash and its treeSize an integer, and the block's inclusion is an object whose leafIndex is an integer and whose path is an array of hashes;
    • RFC 6962 inclusion verification (Tree §3.4) succeeds for the leaf D (its 32 bytes), at index leafIndex, in a tree of size treeSize with root root, along path. A treeSize of 0, or a leafIndex of at least treeSize, fails.
  • A session sign-off binds a record whose expectation is the session form when the assertion presents the statement challenge computed from the expectation's statementDigest.
  • A mandate block binds a record whose expectation is the mandate form when its assertion's assertion digest is mandateAssertion, that assertion presents the statement challenge of the block's statement, and the block's document, when it carries one, has the digest the statement names and, as principal.signer, the statement's signer (§9.6). A block without a document leaves the record's scope and limits undecided (Verification §4.12).

Verification §5 states how a verifier holds the sign-off material to the expectation by these rules.

9.6 Mandates

A mandate lets an agent decide without a human signature on each record. A principal signs it once; each record decided under it names it in its hashed expectation and in a hashed mandate link, and carries the signed material with it.

The mandate block. A record whose expectation is the mandate form carries, as nonRepudiation.mandate, an object with exactly these members:

"mandate": {
  "statement":          { … },                     // the mandate statement, below
  "assertion":          { "signature", "credentialKeyId", "algorithm",
                          "authenticatorData", "clientDataJSON" },   // the principal's WebAuthn assertion
  "assertionTimestamp": "<base64 DER TimeStampResp>" | null,          // a timestamp over the assertion digest
  "document":           { … }                      // the mandate document (§9.7)
}

The assertion's authenticatorData, clientDataJSON and signature are strings that decode as §9.3 states; algorithm is "ES256" or "Ed25519". A block whose document is absent or null carries no document.

The assertion digest names one assertion, whatever encoding of its signature a writer stored:

assertionDigest = hex( SHA-256( UTF-8("tzun-assertion-v1") ‖ SHA-256(authenticatorData)
                                ‖ SHA-256(clientDataJSON) ‖ SHA-256(signature') ) )

over the decoded bytes. For ES256, the decoded signature is the DER encoding (ITU-T X.690: each length in its shortest definite form, each INTEGER in the fewest octets of two's complement, and nothing after the SEQUENCE) of ECDSA-Sig-Value ::= SEQUENCE { r INTEGER, s INTEGER }, with 1 ≤ r ≤ n − 1 and 1 ≤ s ≤ n − 1, where n is the order of the P-256 group. signature' is the DER encoding of (r, s'), with s' = n − s when s > (n − 1) / 2 and s' = s otherwise. A high-S and a low-S signature therefore have one digest. For Ed25519, signature' is the signature as decoded. An assertion with another algorithm, with a member that is missing or does not decode, or with an ES256 signature that is not such a DER encoding, has no assertion digest. A BER encoding that DER forbids, such as a long-form length that fits the short form or an INTEGER with a needless leading 00 octet, is not DER, and so has none.

The mandate statement is a sign-off statement in the value domain (§9.4) with exactly these members:

{ "v": "tzun-signoff-mandate/1", "mode": "mandate",
  "mandateDigest": "<64 lowercase hex>",           // the digest of the mandate document (§9.7)
  "signer": "<the principal's user id>",           // the value grammar of acct (§6.3)
  "signerKind": "human",
  "meaning": "responsibility",
  "policyDigest": "<64 lowercase hex>" | "none",
  "notBefore": "<finalized-block reference (§6.2)>" | "none" }

and these values:

MemberRule
v"tzun-signoff-mandate/1"
mode"mandate"
mandateDigestA bare digest (§1.2)
signerThe value grammar of acct (§6.3)
signerKind"human"
meaning"responsibility"
policyDigestA bare digest, or "none"
notBeforeA finalized-block reference (§6.2), or "none"

A statement with another member, a missing one, or a value outside its rule is not a mandate statement. The statement names the principal by an opaque user id and carries no printed name (§6.8, §9.11).

The principal signs it by making a WebAuthn assertion, with user verification, that presents its statement challenge (§9.4). For the statement

{"mandateDigest":"26b1fbef903914e39cb45378b7f10648611c751c1b93624173bcfe7b7b9938da","meaning":"responsibility","mode":"mandate","notBefore":"eip155:1@23400000:0x9709713cdb609ef086959f42eb4a56159cb9ac637e3f32fb381796e68b2c7efb","policyDigest":"none","signer":"user-2c9d","signerKind":"human","v":"tzun-signoff-mandate/1"}

of the document of §9.7, the statement digest is 9a2e761fd633a06433c2e386fd2107112260da481db7c4c4b5795954a7e69492 and the statement challenge KZTU75Uvu5IeVsZpIpEN1SDqskrp3rA3Dcwl8oMgUBM. An Ed25519 assertion that presents it, made with the key of RFC 8037 Appendix A, has these members:

{
  "algorithm": "Ed25519",
  "credentialKeyId": "kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k",
  "authenticatorData": "Chw32mRsvW5TbBTARkVeu2ErLBpCvZeP6rZmEl_zd5UFAAAAAQ",
  "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiS1pUVTc1VXZ1NUllVnNacElwRU4xU0Rxc2tycDNyQTNEY3dsOG9NZ1VCTSIsIm9yaWdpbiI6Imh0dHBzOi8vZGVtby50enVuLmFpIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ",
  "signature": "QiEu8ZU71lvqBrWt2VOZ-kvFyd4HwtGEQ-h6yE6ndP9DQ2QZC7UqUTwwgvxxIWSepOPQ5EaPcJosrVzkbKfmBA"
}

and the assertion digest 79aafeb32c197be65d3b27c7b1cc469beff89e8650d1a84e02b437ec7937966d.

Issuance. A writer records the issue of a mandate as a record of its own, the issuance record: aiSystemId "tzun:mandate", action "issue-mandate", aiOutput { "mandateDigest", "statementDigest", "assertionDigest" } naming the document, the statement and the principal's assertion, a human decider whose identifier is acct: followed by the statement's signer, contributions null, and the expectation null. Its links are null, or, for a mandate that replaces a predecessor, a supersedes link to the predecessor's issuance record and the root link of §8.5. It carries the mandate block's material, with the same members and the same rules, as nonRepudiation.mandateIssuance and never as mandate. Its material holds together when the document's digest is aiOutput.mandateDigest and the statement's mandateDigest, and its principal.signer is the statement's signer; the statement's digest is aiOutput.statementDigest; the assertion presents the statement challenge; the assertion digest is aiOutput.assertionDigest; and the decider is acct: followed by the statement's signer. The issuance record of the mandate above is

{
  "evidenceId": "7a2c0d4e-1111-4a1a-8a1a-000000000001",
  "schemaVersion": "v1.0",
  "aiSystemId": "tzun:mandate",
  "aiOutput": {
    "assertionDigest": "79aafeb32c197be65d3b27c7b1cc469beff89e8650d1a84e02b437ec7937966d",
    "mandateDigest": "26b1fbef903914e39cb45378b7f10648611c751c1b93624173bcfe7b7b9938da",
    "statementDigest": "9a2e761fd633a06433c2e386fd2107112260da481db7c4c4b5795954a7e69492"
  },
  "action": "issue-mandate",
  "reason": "Claims-triage mandate for Q4 2026",
  "capturedAt": "2026-09-30T14:05:30.000Z",
  "sequenceNumber": 1,
  "decider": {"kind": "human", "id": "acct:user-2c9d", "agent": null, "credential": null, "freshness": null},
  "contributions": null,
  "links": null,
  "signoffExpectation": null
}

with a hashed object of 633 bytes and the digest fb29b130c25c42d6ac72f1ea4123f0c542d6fefed9cfcaaa6b9a1b5e74af9844.

Capture under a mandate. A writer captures a record under a mandate only when the mandate is open (issued, within its validity window, not revoked, not superseded and not full), the decider has the kind agent-mandated and is one of the document's agents, the record's schema version is the document's schemaVersion, and the record lies within the document's scope and limits (§9.9). It takes the next slot, so that the slots of one mandate are 1, 2, 3 and so on with no gap and no repeat, and writes each running total. When a predicate of the document's confirm is true, it writes confirm true; when none is true and one is undeterminable (§9.8), it refuses the capture. It writes a mandate link to the mandate's issuance record. The record decided under the mandate above at its slot 41, with a running total of 5,230,000, is

{
  "evidenceId": "7a2c0d4e-2222-4b2b-9b2b-000000000141",
  "schemaVersion": "v1.0",
  "aiSystemId": "claims-triage-v4",
  "aiOutput": {"amountMinor": 120000, "fraudFlag": false, "recommendation": "approve"},
  "action": "approve",
  "reason": "Policy active; invoice matches the repair estimate",
  "capturedAt": "2026-11-20T10:14:50.000Z",
  "sequenceNumber": 141,
  "decider": {
    "kind": "agent-mandated",
    "id": "desc:sha256:eed92bfa7577d6f66e26fed38c35ebbec3b663a25fd6cc4c6af7d8c7475a250f",
    "agent": "sha256:eed92bfa7577d6f66e26fed38c35ebbec3b663a25fd6cc4c6af7d8c7475a250f",
    "credential": null,
    "freshness": "eip155:1@23652000:0x5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e"
  },
  "contributions": {
    "pipeline": null,
    "externals": [],
    "steps": [
      {
        "role": "propose",
        "actor": {
          "kind": "agent-mandated",
          "id": "desc:sha256:eed92bfa7577d6f66e26fed38c35ebbec3b663a25fd6cc4c6af7d8c7475a250f",
          "agent": "sha256:eed92bfa7577d6f66e26fed38c35ebbec3b663a25fd6cc4c6af7d8c7475a250f"
        },
        "aiBom": {
          "modelDescriptorDigest": "sha256:6c9556bc8e35e7701b44ecee10c605a4db9fdf082e6856681db7d7dd1ca3a947",
          "vendorFingerprint": null,
          "promptTemplate": null,
          "inferenceParameters": null,
          "infrastructure": null
        },
        "inputs": [],
        "output": "sha256:252a3512d90f1214170196efddbaba73a8c02f7a9dd1ba6cb42588ad64d44fe0",
        "verdict": null,
        "reason": null,
        "prev": null
      }
    ]
  },
  "links": [
    {
      "rel": "mandate",
      "target": {
        "digest": "fb29b130c25c42d6ac72f1ea4123f0c542d6fefed9cfcaaa6b9a1b5e74af9844",
        "evidenceId": "7a2c0d4e-1111-4a1a-8a1a-000000000001",
        "locator": {"origin": "test.insurer.example/9a8b7c6d5e4f30211203f4e5d6c7b8a9/evidence/636c61696d73", "seq": 1}
      },
      "attrs": null
    }
  ],
  "signoffExpectation": {
    "mandateAssertion": "79aafeb32c197be65d3b27c7b1cc469beff89e8650d1a84e02b437ec7937966d",
    "mandateSeq": 41,
    "totals": {"paid": 5230000},
    "confirm": false
  }
}

with a hashed object of 1,697 bytes and the digest 057a50035ba3eb6b0ae82b2cb8e1aa8e22bf8a223e5470c135cce67ab58766fe. Its freshness reference is illustrative.

Revocation and succession. A writer records the revocation of a mandate as a revocation record: aiSystemId "tzun:mandate", action "revoke-mandate", aiOutput { "mandateDigest", "statementDigest", "claimedRevocationInstant", "reason" } naming the mandate's document and statement, the instant from which the revocation claims to take effect (a date-time with three fractional digits, no later than the record's capturedAt) and the reason, a human decider naming who revoked it, a revokes link to the mandate's issuance record, and the expectation null. A mandate whose document names another document's digest as its predecessor replaces that mandate, and its issuance record has a supersedes link to the predecessor's. A writer captures no record under a mandate once it is revoked or replaced.

Confirmation. A record whose expectation has confirm true owes a human's confirmation. nonRepudiation.confirmation holds it: a WebAuthn assertion, with user verification, that presents the record's confirmation challenge (§9.4). It is added after capture, and it changes no digest.

9.7 The mandate document tzun-mandate/1

The document is what the principal grants. Its digest is

mandateDigest = hex( SHA-256( C(document) ) )

and its canonical bytes are at most 16 KiB (16,384 bytes); a larger document is malformed. It is an object with exactly these members, every one present, at every depth:

{
  "v": "tzun-mandate/1",
  "mandateId": "<UUID>",
  "principal": { "signer": "<the principal's user id>", "credential": "<64 hex>" | null },
  "agents": ["<decider identifier>", …],          // the agents that may decide under it
  "schemaVersion": "v1.0",
  "scope": {
    "aiSystemIds": ["…"] | null,                  // null: no restriction; []: permits nothing
    "actions":     ["…"] | null,
    "models":      ["sha256:<64 hex>"] | null,     // model descriptor digests
    "predicates":  { "<name>": <predicate>, … } | null
  },
  "confirm":    { "<name>": <predicate>, … } | null,
  "validity":   { "notBefore": "<RFC 3339 UTC>" | null, "notAfter": "<RFC 3339 UTC>" | null },
  "limits":     { "maxRecords": <integer> | null,
                  "cumulative": { "<cap name>": { "path": "<JSON Pointer>", "max": <integer> }, … } | null },
  "delegation": { "allowed": false, "maxDepth": 0, "attenuation": "subset" },
  "review":     { "maxAgeMs": <integer> } | null,
  "policyDigest": "<64 hex>" | null,
  "predecessor":  "<64 hex>" | null,              // the digest of the document this one replaces
  "parent":       "<64 hex>" | null
}

Grammar.

  • Every string, at every depth, is printable ASCII, and every number an integer (§1.2).
  • Every array is a set (§1.2). A writer sorts it; a reader never does.
  • principal.signer follows the value grammar of acct (§6.3), and is an opaque user id (§6.8): the issuance record's decider is acct: followed by it. principal.credential is null or a bare digest, a registration this version does not read.
  • agents holds decider identifiers (§6.2) of the schemes of an agent (§6.4).
  • limits.cumulative is null or holds 1 to 16 caps, each named by a cap name (§9.2). An empty object is malformed: a document with no cap writes null.
  • A document holds at most 64 predicates, counting those of scope.predicates and confirm together. A predicate's name follows the grammar of a cap name.
  • delegation is allowed false, maxDepth 0 and attenuation "subset", and parent is null: this version defines no mandate issued under another mandate. A format for such mandates takes a v of its own, which a reader of this version abstains on (Verification §4.12).
  • Each bound of validity is null or a string matching /[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z/ that names a real instant in UTC: a month from 01 to 12, a day that month has in that year, an hour from 00 to 23, and a minute and a second from 00 to 59.
  • review is carried for a later version; this version does not read it.
  • The restriction. schemaVersion names a schema version whose hashed field set holds aiSystemId, action and contributions; in this version that is v1.0. Every predicate's path, in scope.predicates and in confirm, and every cap's path has at least one reference token, and its first reference token is a member of that version's hashed field set (§4.1). Whatever a mandate constrains is therefore covered by the record's digest. A path into a step's bill of materials, such as /contributions/steps/0/aiBom/inferenceParameters/temperature, meets the restriction.

A document that breaks any rule of this grammar is malformed.

Example. The document

{"agents":["desc:sha256:eed92bfa7577d6f66e26fed38c35ebbec3b663a25fd6cc4c6af7d8c7475a250f"],"confirm":{"fraud":{"op":"eq","path":"/aiOutput/fraudFlag","quant":null,"value":true}},"delegation":{"allowed":false,"attenuation":"subset","maxDepth":0},"limits":{"cumulative":{"paid":{"max":250000000,"path":"/aiOutput/amountMinor"}},"maxRecords":10000},"mandateId":"6b1f0e2a-7c3d-4e5f-8a9b-0c1d2e3f4a5b","parent":null,"policyDigest":null,"predecessor":null,"principal":{"credential":null,"signer":"user-2c9d"},"review":{"maxAgeMs":604800000},"schemaVersion":"v1.0","scope":{"actions":["approve","refer"],"aiSystemIds":["claims-triage-v4"],"models":["sha256:6c9556bc8e35e7701b44ecee10c605a4db9fdf082e6856681db7d7dd1ca3a947"],"predicates":{"cap":{"op":"le","path":"/aiOutput/amountMinor","quant":null,"value":500000}}},"v":"tzun-mandate/1","validity":{"notAfter":"2026-12-31T23:59:59.999Z","notBefore":"2026-10-01T00:00:00.000Z"}}

(921 bytes) lets the agent of §6.9 approve or refer claims of up to 500,000 minor units each, up to 250,000,000 in total and 10,000 records, until the end of 2026, and asks for a human confirmation of any claim flagged as fraud. Its digest is 26b1fbef903914e39cb45378b7f10648611c751c1b93624173bcfe7b7b9938da, the mandateDigest of the statement of §9.6.

9.8 Predicates

A predicate is an object with exactly the members path, op, value and quant:

  • path is an RFC 6901 JSON Pointer into the payload. ~0 and ~1 are its only escapes; any other ~ makes the predicate malformed. On an array, a reference token is an index, 0 or /[1-9][0-9]*/; the token -, and any other token, resolves to nothing.
  • op is one of eq, ne, lt, le, gt, ge, in, notIn, prefix and glob.
  • quant is null, "all" or "any".

Values. A string is a JSON string, compared by code points. An integer is a JSON number read under the integer rule (§1.2): 1.0 and 1e0 are the integer 1, and a negative, fractional or larger number is not an integer. A boolean is true or false (§1.2): the number 1 is never true.

Operations. A predicate is true, false or undeterminable:

opvalueSubjectTrue when
eq, nea string, an integer or a booleananythe subject has the same type and the same value (eq), or not (ne)
lt, le, gt, gean integeran integerthe comparison holds
in, notIna set of 1 to 64 stringsa stringthe subject is a member (in), or not (notIn)
prefixa stringa stringthe subject begins with value
globa set of 1 to 64 patterns, each of 1 to 256 charactersa stringthe subject matches one of the patterns

A value that does not suit its op, null included, makes the predicate malformed. The predicate is undeterminable when its path resolves to nothing or to null; when the subject is of a type the operation does not take; when quant is null and the subject is an array, or quant is not null and the subject is not an array; when a string subject is longer than 4,096 code points; or when an array subject has more than 10,000 elements.

Quantifiers. With quant, the operation applies to each element of the array subject, and an element that is null is undeterminable. Under all, [] is true; any element false makes the predicate false; otherwise any element undeterminable makes it undeterminable; otherwise it is true. Under any, [] is false; any element true makes the predicate true; otherwise any element undeterminable makes it undeterminable; otherwise it is false.

Glob. Pattern and subject are read as code points and split at / into segments; the whole subject must match. A segment ** matches any run of whole segments, none included. Within a segment, * matches any run of code points and ? matches one code point, and every other code point matches itself. Informative: an implementation matches ** segment by segment, and the rest of a segment with the iterative two-pointer wildcard algorithm, which needs no recursion; the worst case is the product of the two lengths, which the limits above bound.

9.9 Scope and limits of one record

These checks read a record decided under a mandate and the document in its mandate block. They need no key.

Scope. Each constraint of the document holds, fails or is undeterminable:

  • agents: the decider's id is a member of agents, or desc: followed by the decider's agent is. A cmt decider is therefore a member only under the same cmt identifier;
  • schemaVersion: the record's schemaVersion is the document's;
  • aiSystemIds, actions: the record's aiSystemId, and its action, are members of the set, when it is not null;
  • models: every model the record names is a member of scope.models, when it is not null: the modelDescriptorDigest of every step's aiBom that is not null. A model outside the set may not propose or review either. A set that is not null, on a record that names no model, is undeterminable, and so is one on a record with a step's aiBom that is neither null nor an object whose modelDescriptorDigest is a string, since the model it names cannot be read;
  • predicates: each predicate of scope.predicates is true (§9.8). A predicate whose path begins with a member outside the record's hashed field set, which a well-formed document of this version never names (§9.7), is undeterminable, never true.

The record is within its mandate when every constraint holds, outside when one fails, and undeterminable otherwise. A record whose confirm is false while a predicate of the document's confirm is true has evaded confirmation.

Limits. For a record whose expectation has slot mandateSeq and running totals totals:

  • mandateSeq is at most limits.maxRecords, when that is not null;
  • totals has exactly one member for each cap of limits.cumulative, and is null when that is null;
  • for each cap, the value at its path in the payload is an integer, the running total is at least that value, and the running total is at most the cap's max.

9.10 Consumption over several records

Informative for a verifier of one record. A verifier that holds every record of a mandate can check that no slot is held twice, that the slots run from 1 with no gap, and that each running total is the one before it plus the record's own value at the cap's path. A missing slot, and a total that jumps by more than the records that remain explain, show that a record decided under the mandate is missing. Whether the last slot is the last one taken needs the writer's own account of the mandate, which lies outside the records.

Whether a mandate was in force when a record was captured, against its validity window, its revocation or its replacement by a successor, is not decided by this version.

9.11 What a mandated record discloses

Each record decided under a mandate carries the whole mandate block to whoever holds the record. That includes the principal's opaque user id (signer) and the document's scope, caps and confirmation rules. The statement carries no printed name of the principal (§9.6). A writer SHOULD state in a mandate only what its records may disclose.

10. The record's RFC 3161 token

nonRepudiation.rfc3161Token, when present, is a timestamp over the record digest, from a timestamp authority the writer chose:

  • The writer sends an RFC 3161 TimeStampReq whose messageImprint is id-sha256 (2.16.840.1.101.3.4.2.1) with hashedMessage equal to the 32 bytes of the digest, and with certReq true, so that the authority includes its signing certificate.
  • The writer stores the DER TimeStampResp as a base64 string. These are the writer rules of Anchor §7.3, with the digest in place of P.
  • The token then proves that the record's canonical bytes existed by its genTime, as attested by an authority whose certificate chains to a root the verifier trusts. It proves nothing about who asked for it.

A verifier checks the token by the steps of Anchor §10.5, with the record's recomputed digest in place of P, as two checks: ts.record.token for steps 1 to 3, and ts.record.trust for step 4 (Verification §4.4). The facts genTime and authority come from the token itself; rfc3161Metadata is never read.

11. Size limits

A writer limit is one a writer refuses to exceed; a verifier still verifies a larger value from another writer. A grammar limit is part of the grammar of its member: a value beyond it is malformed.

ItemLimitKind
The hashed object, as canonical bytes256 KiB (262,144 bytes)writer
The mandate block, and the issuance block, each as canonical bytes48 KiB (49,152 bytes)writer
action128 characterswriter
reason, and a step's reason16,384 code unitswriter
Each string of a step's aiBom.infrastructure256 characterswriter
A step's aiBom.inferenceParameters.stopSequences16 strings of at most 256 characters eachwriter
An agent descriptor16 KiB as canonical bytes; 64 elements in each array; 256 code units in each string, 8,192 in an attestationwriter
The id of a decider or of an actor1,024 charactersgrammar
steps32grammar
inputs of one step, and externals64 eachgrammar
role56 characters: the class propose, : and a name of 48grammar
links256grammar
rel32 characters for a registered name, 67 for an extension namegrammar
target.evidenceId128 charactersgrammar
target.locator.origin253 characters in its namespace (Anchor §4.3.2); its other segments have no limit of their owngrammar
attrs of one link16 members; a name of 32 characters, or 67 for an extension name; a string value of 256 charactersgrammar
Caps, in totals and in limits.cumulative16grammar
The mandate document, as canonical bytes16 KiB (16,384 bytes)grammar
Predicates in one mandate document64grammar
Elements of an in, notIn or glob set64grammar
A glob pattern256 charactersgrammar
A predicate's subject4,096 code points, or an array of 10,000 elementsundeterminable beyond (§9.8)