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
| Term | Meaning |
|---|---|
| record | One DEO: a JSON object called the envelope (§2) |
| writer | Software that creates a record or adds to its nonRepudiation block |
| verifier | Software that checks a record against this specification |
| chain | The records that share one chain id, numbered by sequenceNumber from 1 (§5) |
| digest | A record's canonicalDigest (§4) |
| decider | Who took the decision a record states: a human, an agent, an agent deciding under a mandate, or a policy engine (§6) |
| agent descriptor | A document that describes an agent: its operator, identities, keys, models, instructions, tools and policy. A record names one by its digest (§6.9). |
| contribution | What one contributor did toward a decision: propose, review, decide or another act. A record lists its contributions as steps (§7). |
| step | One 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 materials | The description of the model that drove a step, by the digest of the model's descriptor (§7.4) |
| link | A statement in a record of how its decision relates to another record, the link's target, under a named relation (§8) |
| link record | A record whose links state relations between other records, made after both exist (§8.6) |
| mandate | A 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) |
| principal | The human who signs a mandate (§9.7) |
| mandate statement | The statement a principal signs to issue a mandate (§9.6) |
| issuance record | The record a writer makes when a mandate is issued (§9.6) |
1.2 Notation
SHA-256is the hash function of FIPS 180-4.‖joins byte strings.UTF-8(s)is the UTF-8 encoding of the strings.hex(x)writes the bytesxas lowercase hexadecimal, two digits a byte.B64URL(x)is the base64url encoding ofx(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
0to9andatof. A prefixed digest issha256: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 insidecontributionsuse 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.0and3e0read as 3; a string such as"3"and a boolean are never integers. The value is the one §3.1 reads:1.0000000000000001reads as 1 and is an integer, and9007199254740993reads as 2^53 and is not. An implementation that keeps numbers as decimal text (Go'sjson.Number) converts each to the nearest binary64 value before it applies this rule. - A boolean is the JSON literal
trueorfalse, and nothing else:1and"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 isre.fullmatchwithre.ASCII, neverre.matchwith$. - 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
nullcounts 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 valuev(§3).
2. The envelope
2.1 Members
| Member | Type | Content |
|---|---|---|
envelopeVersion | string | "e1" |
payload | object | The decision and its context (§2.2) |
integrity | object | The digest and the chain link (§2.3) |
nonRepudiation | object, optional | Signatures, timestamps and the anchor (§2.4) |
custodyChain | array, optional | Custody information. This version of the specification does not define its content, and does not verify it (Specs §4). |
attestationStatus | string, optional | A 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
| Member | Hashed | Type | Content |
|---|---|---|---|
evidenceId | yes | string | The record's identifier, a UUID assigned by the writer |
schemaVersion | no | string | "v1.0". It selects the hashed field set and the serialization rules (§4.3). |
aiSystemId | yes | string | The AI system, or the process, whose output the decision concerns |
aiOutput | yes | any JSON value | The output as presented to the decider |
action | yes | string | The decision taken, whoever took it, such as approve |
reason | yes | string | The reason stated for the decision, whoever stated it |
capturedAt | yes | string | The instant of capture, an RFC 3339 timestamp in UTC |
sequenceNumber | yes | integer, at least 1 | The record's position in its chain (§5) |
decider | yes | object or null | Who decided (§6) |
contributions | yes | object or null | The steps that led to the decision, and the model behind each (§7) |
links | yes | array or null | The record's relations to other records (§8) |
signoffExpectation | yes | null or object | The 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:
aiSystemIdis a non-empty string. The valuestzun:mandateandtzun:linkname the records a writer makes about mandates (§9.6) and the link records it makes (§8.6), and every value that begins withtzun:is reserved: a writer MUST NOT take such a value from its caller.aiOutputis a JSON object.actionis 1 to 128 characters of printable ASCII, with no space at its start or its end.reasonis 1 to 16,384 code units long and holds no lone surrogate.decideris notnull(§6.7).- a decision on the output of a model records the model in a step of
contributions(§7.3). linksisnullwhen the record has no link, never an empty array (§8.2).- the size limits of §11 hold.
2.3 The integrity block
| Member | Type | Content |
|---|---|---|
canonicalDigest | hash | The record digest (§4.2) |
chainLink | hash, or absent | The 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.
| Member | Content | Specified in |
|---|---|---|
rfc3161Token | The record's own RFC 3161 timestamp token | §10 |
rfc3161Metadata | A writer's copy of some of that token's fields, for display. Never an input to verification. | — |
webauthnAssertion | A WebAuthn assertion made by a human signer. This version reads only its clientDataJSON. | §9.3 |
signoff | A sign-off block | §9.3 |
assertionTimestamp | A timestamp over the assertion. Not verified by this version. | Verification §4.7 |
mandate | The mandate block of a record decided under a mandate: the mandate document, the principal's statement and assertion | §9.6 |
mandateIssuance | The same material, carried by the record that issues a mandate | §9.6 |
confirmation | A human's WebAuthn assertion confirming a record decided under a mandate | §9.6 |
anchor | The record's anchor artifact | Anchor §8 |
blockchainAnchor | A legacy anchor form, which verifiers abstain on | Anchor §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
Crefuses (§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 thatCcan 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,trueandfalseas 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:
| Character | Written 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:
- If
xis zero, of either sign, write0. - If
xis negative, write-and then−xby these steps. - Otherwise find integers
n,kandssuch thatsis written with exactlykdecimal digits, the first of them not zero,s × 10^(n−k)equalsx, andkis the least for which such ansexists. If several values ofsqualify, take the one that makess × 10^(n−k)nearest tox, and between two equally near the even one. Write the digits ofsas follows:k ≤ n ≤ 21: the digits, thenn − kzeros (1000000,100000000000000000000);0 < n < k: the firstndigits,., the otherk − ndigits (12.5);−6 < n ≤ 0:0., then−nzeros, then the digits (0.5,0.001);- otherwise, exponent form: the first digit; if
k > 1,.and the other digits; thene,+ifn − 1is 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
Cas 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
nulland 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 isoutputDigest({ "action": "approve", "reason": <the record's reason> })(§7.3); - the step digests are
sha256:32a1ccc01f6eb79975fef13137aca3f4984454bf779f62218b82a630c0d86a10,sha256:c8eb7215f2d39fa10a06375e225c0b35e9dd2dec2cac1314fa8b023396ff7d8bandsha256:a355a0cefe7b2020f5ff81eec4aef5a51349260813791f84b62935e4053485fd, and each step'sprevis 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
rootlink 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
sequenceNumber1 has nochainLink. - A record with
sequenceNumbern > 1 carries the link to its predecessor, the record of the same chain withsequenceNumbern − 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
}
| Kind | Who decided |
|---|---|
human | A person |
agent | An automated agent, on its own authority |
agent-mandated | An automated agent, under a mandate a principal signed (§9.6) |
policy-engine | A 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:
| Member | Rule |
|---|---|
kind | A 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. |
id | 3 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. |
agent | null, or a prefixed digest (§1.2). |
credential | null, or a bare digest (§1.2). This version does not read the registration it names. |
freshness | null, 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
| Scheme | Value | Writer rule |
|---|---|---|
acct | The relying party's own opaque account identifier for the person, as in acct:u-4821 | The value matches /[A-Za-z0-9._-]{1,128}/. It has no @, so an email address is never an identifier. |
spiffe | A 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 .. |
jkt | The RFC 7638 thumbprint of the decider's public key | Exactly 43 characters of base64url |
desc | The agent descriptor's prefixed digest | Equal to agent |
cmt | A commitment to an identifier of another scheme | A 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:
kind | agent | Schemes permitted |
|---|---|---|
human | null | acct, cmt |
agent, agent-mandated | not null | spiffe, jkt, desc, cmt |
policy-engine | not null | spiffe, 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 kind | Expectation null, or the policy form | The session form | The mandate form |
|---|---|---|---|
human | agrees | agrees | contradicted |
agent, policy-engine | agrees | contradicted | contradicted |
agent-mandated | contradicted | contradicted | agrees |
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
acctdecider 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
}
]
}
| Member | Rule |
|---|---|
pipeline | null, or a prefixed digest. This version does not define pipeline definitions or read the one named. |
externals, inputs | Sets (§1.2) of prefixed digests, each of 0 to 64 elements |
role | A class, one of propose, review, decide and other, optionally followed by : and a name matching /[A-Za-z0-9._-]{1,48}/ |
actor | null, 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. |
aiBom | null, or an object. A writer writes the object of §7.4; a reader checks none of its members (§7.4). |
output, prev | Prefixed digests; prev may be null |
verdict | null, or one of the three strings |
reason | null, 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.
externalsand everyinputsare sets. A writer that sorts them does so before it computes any digest; a reader never sorts. - Verdicts.
verdictisnullfor the classesproposeandother. - 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 classproposeorreviewfollows a step of the classdecide; a step of the classothermay, such as one that carries the decision out. - The step chain.
prevof step 0 isnull, andprevof each later stepiisstepDigest(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
deciderand the deciding step'sactorhave the samekind,idandagent, compared as canonical bytes. A writer that hides the decider behind a commitment writes the samecmtidentifier in both. - The decision itself. The
outputof a step of the classdecideisoutputDigest({ "action": <action>, "reason": <reason> }), built from the record's ownactionandreason, withnullfor 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
nullfor a value it does not have. A missing member and anullone give different canonical bytes, so a writer that fills in absent members MUST do so before it computes the digest. modelDescriptorDigestis a prefixed digest (§1.2) of the model's descriptor, a document this version does not define or read.- A verifier hashes
aiBomas it stands, whatever members it has, and checks none of them. The one member a rule of this version reads ismodelDescriptorDigest, 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 classproposebefore the deciding step, or, when there is no deciding step, the last step of the classpropose. A step of the classreviewcovers the output of the lastproposestep before it; a review with noproposestep before it covers none. - Input provenance. Each input of step
iis theoutputof an earlier step,sha256:followed by thetarget.digestof one of the record's links, or an element ofexternals. Links declare inputs only whenlinksmeets 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 ofP*, and, when there is a deciding step, itsinputsinclude the output ofP*: what was decided on is what was proposed and reviewed. With noP*, 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
| Item | Rule |
|---|---|
links | null, or an array of 1 to 256 links. An empty array is malformed: "no link" has one spelling, null. |
| link | An 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. |
target | An object with exactly digest, evidenceId and locator. |
target.digest | A bare digest (§1.2). |
target.evidenceId | null, 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.locator | null, 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). |
attrs | null, 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. |
| Order | The 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
linksmember together with the record'saiSystemId,action,contributionsandsignoffExpectation, 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.
| Name | Rule |
|---|---|
mandate_link | Exactly 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_link | Exactly one revokes link when the record is a revocation record (§9.6: aiSystemId "tzun:mandate" and action "revoke-mandate"), and none otherwise. |
subject_link | Exactly 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_one | At 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.
rel | This record… | attrs | Record-local rule | Target rule |
|---|---|---|---|---|
derivedFrom | uses the target's decision, or its output, as an input | null | — | — |
reviews | is a review of the target's decision | null | — | 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. |
supersedes | replaces the target's decision on the same matter | null | — | supersedes_same_system: the target's aiSystemId is this record's. |
appeals | records an appeal against, or a contest of, the target's decision | null | — | — |
ratifies | approves, after the fact, a decision the target took on its own authority | null | ratifies_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. |
implements | carries out the target's decision | null | — | — |
delegatedBy | is taken under authority the target delegated | null | — | That the target carries the authority delegated. This version does not define the rule, and a verifier abstains on it. |
closes | closes 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. |
fanoutOf | is 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 |
root | belongs to the matter whose first decision is the target (§8.5) | null | root_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. |
mandate | is 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. |
revokes | revokes 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. |
subject | is 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
evidenceIdwhen the caller gives one, and bydigestotherwise, and refuses a target it does not hold. It refuses a member the caller gave that disagrees with the record it holds: adigestthat is not that record's stored digest, anevidenceIdthat is not its evidence id, alocator.originthat is not the origin of its chain's evidence log, and alocator.seqthat is not itssequenceNumber. It fills indigest,evidenceIdandlocatorfrom the record it holds, except that it writeslocatorasnullwhen 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,
digestis required, andevidenceIdandlocatorare written as the caller obtained them from the store that holds the target, ornull. 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,revokesandsubjectlinks 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 noclosesorfanoutOflink (§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 writesnullunless 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, sincenonRepudiationis not hashed. This version does not interpret the policy thatpolicyDigestnames. - The session form: the record is captured inside a signed session.
statementDigestis the digest (§9.4) of the session's sign-off statement, andsessionSeqthe record's slot in the session, counting from 1. Slots need not be contiguous. A writer MUST store a session record'ssignoffblock (§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).
mandateAssertionis the assertion digest (§9.6) of the principal's assertion that issued the mandate;mandateSeqis the record's slot in the mandate, counting from 1;totalsholds, for each cumulative cap of the mandate, the running total including this record, and isnullwhen the mandate has none;confirmsays whether a human confirmation of the record is owed. A writer MUST store the record'smandateblock (§9.6) with the record itself, in the same write, and the record has amandatelink 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,
statementDigestandsessionSeq; - an object with exactly four members,
mandateAssertion,mandateSeq,totalsandconfirm.
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.
webauthnAssertionwhen 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 challengecwhen that string decodes, the result parses (§3.1) as a JSON object, and the object'schallengemember is the stringc. The object'stypemember is the assertion's type. An assertion whoseclientDataJSONis 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'sassertionand theconfirmation(§9.6). - Sign-off block. The value of
signoffwhen it is notnull, of whatever JSON type. When it is an object, its members arestatement, a sign-off statement (§9.4);inclusion, an object{ "leafIndex": <integer>, "path": [<hash>, …] }, for a batch; andsessionSeq, 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
sessionSeqmember or has astatementthat is an object whosemodeis the string"session". It is a session sign-off whether or not an assertion accompanies it. - Mandate block. The value of
mandatewhen it is notnull, of whatever JSON type (§9.6). - Issuance block. The value of
mandateIssuancewhen it is notnull, 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 ofD. - A batch sign-off binds the record when all of these hold:
- the sign-off block and its
statementare both objects, the statement lies in the value domain (§9.4), and itsmodeis"batch"; - the assertion presents that statement's challenge;
- the statement's
rootis a hash and itstreeSizean integer, and the block'sinclusionis an object whoseleafIndexis an integer and whosepathis an array of hashes; - RFC 6962 inclusion verification (Tree §3.4) succeeds for the leaf
D(its 32 bytes), at indexleafIndex, in a tree of sizetreeSizewith rootroot, alongpath. AtreeSizeof 0, or aleafIndexof at leasttreeSize, fails.
- the sign-off block and its
- 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, asprincipal.signer, the statement'ssigner(§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:
| Member | Rule |
|---|---|
v | "tzun-signoff-mandate/1" |
mode | "mandate" |
mandateDigest | A bare digest (§1.2) |
signer | The value grammar of acct (§6.3) |
signerKind | "human" |
meaning | "responsibility" |
policyDigest | A bare digest, or "none" |
notBefore | A 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.signerfollows the value grammar ofacct(§6.3), and is an opaque user id (§6.8): the issuance record's decider isacct:followed by it.principal.credentialisnullor a bare digest, a registration this version does not read.agentsholds decider identifiers (§6.2) of the schemes of an agent (§6.4).limits.cumulativeisnullor holds 1 to 16 caps, each named by a cap name (§9.2). An empty object is malformed: a document with no cap writesnull.- A document holds at most 64 predicates, counting those of
scope.predicatesandconfirmtogether. A predicate's name follows the grammar of a cap name. delegationisallowedfalse,maxDepth0 andattenuation"subset", andparentisnull: this version defines no mandate issued under another mandate. A format for such mandates takes avof its own, which a reader of this version abstains on (Verification §4.12).- Each bound of
validityisnullor 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. reviewis carried for a later version; this version does not read it.- The restriction.
schemaVersionnames a schema version whose hashed field set holdsaiSystemId,actionandcontributions; in this version that isv1.0. Every predicate'spath, inscope.predicatesand inconfirm, and every cap'spathhas 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:
pathis an RFC 6901 JSON Pointer into the payload.~0and~1are its only escapes; any other~makes the predicate malformed. On an array, a reference token is an index,0or/[1-9][0-9]*/; the token-, and any other token, resolves to nothing.opis one ofeq,ne,lt,le,gt,ge,in,notIn,prefixandglob.quantisnull,"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:
op | value | Subject | True when |
|---|---|---|---|
eq, ne | a string, an integer or a boolean | any | the subject has the same type and the same value (eq), or not (ne) |
lt, le, gt, ge | an integer | an integer | the comparison holds |
in, notIn | a set of 1 to 64 strings | a string | the subject is a member (in), or not (notIn) |
prefix | a string | a string | the subject begins with value |
glob | a set of 1 to 64 patterns, each of 1 to 256 characters | a string | the 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
idis a member ofagents, ordesc:followed by the decider'sagentis. Acmtdecider is therefore a member only under the samecmtidentifier; - schemaVersion: the record's
schemaVersionis the document's; - aiSystemIds, actions: the record's
aiSystemId, and itsaction, are members of the set, when it is notnull; - models: every model the record names is a member of
scope.models, when it is notnull: themodelDescriptorDigestof every step'saiBomthat is notnull. A model outside the set may not propose or review either. A set that is notnull, on a record that names no model, is undeterminable, and so is one on a record with a step'saiBomthat is neithernullnor an object whosemodelDescriptorDigestis a string, since the model it names cannot be read; - predicates: each predicate of
scope.predicatesis 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:
mandateSeqis at mostlimits.maxRecords, when that is notnull;totalshas exactly one member for each cap oflimits.cumulative, and isnullwhen that isnull;- for each cap, the value at its
pathin the payload is an integer, the running total is at least that value, and the running total is at most the cap'smax.
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
TimeStampReqwhosemessageImprintisid-sha256(2.16.840.1.101.3.4.2.1) withhashedMessageequal to the 32 bytes of the digest, and withcertReqtrue, so that the authority includes its signing certificate. - The writer stores the DER
TimeStampRespas a base64 string. These are the writer rules of Anchor §7.3, with the digest in place ofP. - 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.
| Item | Limit | Kind |
|---|---|---|
| The hashed object, as canonical bytes | 256 KiB (262,144 bytes) | writer |
| The mandate block, and the issuance block, each as canonical bytes | 48 KiB (49,152 bytes) | writer |
action | 128 characters | writer |
reason, and a step's reason | 16,384 code units | writer |
Each string of a step's aiBom.infrastructure | 256 characters | writer |
A step's aiBom.inferenceParameters.stopSequences | 16 strings of at most 256 characters each | writer |
| An agent descriptor | 16 KiB as canonical bytes; 64 elements in each array; 256 code units in each string, 8,192 in an attestation | writer |
The id of a decider or of an actor | 1,024 characters | grammar |
steps | 32 | grammar |
inputs of one step, and externals | 64 each | grammar |
role | 56 characters: the class propose, : and a name of 48 | grammar |
links | 256 | grammar |
rel | 32 characters for a registered name, 67 for an extension name | grammar |
target.evidenceId | 128 characters | grammar |
target.locator.origin | 253 characters in its namespace (Anchor §4.3.2); its other segments have no limit of their own | grammar |
attrs of one link | 16 members; a name of 32 characters, or 67 for an extension name; a string value of 256 characters | grammar |
Caps, in totals and in limits.cumulative | 16 | grammar |
| The mandate document, as canonical bytes | 16 KiB (16,384 bytes) | grammar |
| Predicates in one mandate document | 64 | grammar |
Elements of an in, notIn or glob set | 64 | grammar |
| A glob pattern | 256 characters | grammar |
| A predicate's subject | 4,096 code points, or an array of 10,000 elements | undeterminable beyond (§9.8) |