From 9d8e806438e158cfa9fd2d2391913b60a2018d82 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Wed, 21 Oct 2026 05:13:09 -0400 Subject: [PATCH 1/7] wai.ai.usage: AI usage statements bound to the bytes, the consumption claim class, and its corpus The usage statement carries draft-ietf-aipref-vocab-08's Structured Field value, the TDM Reservation Protocol's properties and the Training and Data Mining Assertion 1.1's entries verbatim. It rides in an envelope's manifest (signed by the object's receipt), a room's WAU1 record and a grant's terms; HTTP fields and a MoQ catalog field are checked against it. One precedence rule: a grant's terms, then the signed statements, most restrictive first. The jwp-receipts claim class consumption carries an agent's statement of what it consumed, under which term, having seen which statements; a gate can require claims before serving an agent. Co-Authored-By: Claude Opus 5.5 --- wai/extensions/ai-usage.md | 544 +++++ wai/live-conformance/usage/vectors.json | 2202 +++++++++++++++++++ wai/wai-rs/Cargo.toml | 18 + wai/wai-rs/src/ai_usage.rs | 992 +++++++++ wai/wai-rs/src/ai_usage/corpus.rs | 1184 ++++++++++ wai/wai-rs/src/ai_usage/sf.rs | 391 ++++ wai/wai-rs/src/bin/wai_ai_usage_vectors.rs | 74 + wai/wai-rs/src/claim_receipt.rs | 47 +- wai/wai-rs/src/claim_receipt/consumption.rs | 229 ++ wai/wai-rs/src/http_binding/receipts.rs | 3 + wai/wai-rs/src/lib.rs | 9 + wai/wai-rs/tests/ai_usage.rs | 61 + 12 files changed, 5750 insertions(+), 4 deletions(-) create mode 100644 wai/extensions/ai-usage.md create mode 100644 wai/live-conformance/usage/vectors.json create mode 100644 wai/wai-rs/src/ai_usage.rs create mode 100644 wai/wai-rs/src/ai_usage/corpus.rs create mode 100644 wai/wai-rs/src/ai_usage/sf.rs create mode 100644 wai/wai-rs/src/bin/wai_ai_usage_vectors.rs create mode 100644 wai/wai-rs/src/claim_receipt/consumption.rs create mode 100644 wai/wai-rs/tests/ai_usage.rs diff --git a/wai/extensions/ai-usage.md b/wai/extensions/ai-usage.md new file mode 100644 index 000000000..043e36806 --- /dev/null +++ b/wai/extensions/ai-usage.md @@ -0,0 +1,544 @@ +# WAI Extension: AI Usage Preferences and Consumption Claims + +> Status: Draft. Registers the record `wai.ai.usage`: AI usage preferences for media, bound to +> the bytes they govern, and the signed statement of an agent that consumed media. A **usage +> statement** carries, verbatim, the preferences of three published vocabularies: the AI usage +> preference vocabulary of `draft-ietf-aipref-vocab-08`, in its exemplary Structured Field form; +> the TDM Reservation Protocol's `tdm-reservation` and `tdm-policy` (Final Community Group +> Report `CG-FINAL-tdmrep-20240510`); and the entries of the `cawg.training-mining` assertion +> (Training and Data Mining Assertion, version 1.1). WAI adds no category to any of them. The +> statement rides in an envelope's manifest, where the object's receipt signs it with the +> content, so it cannot be removed without the object failing its receipt; in a room usage +> record, `WAU1`, that the room authority signs; and in a room grant, as the terms one principal +> is granted. An HTTP origin writes the `Content-Usage` field of `draft-ietf-aipref-attach-05` +> and the TDMRep fields from the signed statement, and a MoQ catalog states it for a track +> before a sink subscribes. One precedence rule decides a use: a grant's terms, then every +> signed statement, the most restrictive governing, then unsigned statements where nothing is +> signed. An agent that consumed objects signs a **consumption claim**, the +> [jwp-receipts](jwp-receipts.md) claim class `consumption`, naming the objects by content hash, +> the term it acted under in each vocabulary, its key as a `did:key`, the statements it saw and +> the grant it held; a publisher verifies it against the objects, and a relay or gateway MAY +> require one before it serves an agent. No new transport. The reference implementation is the +> `ai_usage` module of `wai-rs` (feature `ai_usage`), with the class's body in `claim_receipt`; +> the corpus is [`live-conformance/usage/`](../live-conformance/usage/), and its second +> implementation is `live-conformance/ai_usage_verify.py`. +> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174. +> +> Texts read for this revision, each pinned to the version named: +> `draft-ietf-aipref-vocab-08` (14 September 2026) and `draft-ietf-aipref-attach-05` +> (19 August 2026), Internet-Drafts of the IETF AI Preferences Working Group, both Working +> Group documents intended for Proposed Standard and neither yet submitted to the IESG; the +> vocabulary marks its §3 and §4 as not yet having the group's consensus. RFC 9651 +> (Structured Field Values for HTTP). The TDM Reservation Protocol, Final Community Group +> Report of 10 May 2024, the report's latest. The Creator Assertions Working Group's Training +> and Data Mining Assertion, version 1.1, ratified 16 May 2025, the current version. A section +> number of one of these texts is that version's. + +## 1. Scope + +In scope: the usage statement, its three vocabularies, how each is read and what each says +about a use (§3); where a statement is carried: an envelope, a room, a grant, an HTTP response +and a MoQ catalog (§4); the precedence rule and what a conflict means (§5); the consumption +claim (§6); the gate a relay or gateway applies before serving an agent (§7); and verification +(§8). + +Out of scope: whether a preference is followed, which the vocabulary leaves to its recipient +(`draft-ietf-aipref-vocab-08` §3.2), and which this extension makes evidence of, not a duty; +the legal effect of a preference or of a claim; how a crawler or an agent is recognised as one +(§7.1); a profile of the agent protocols that fetch media, which this revision does not define; +and the robots.txt rule of `draft-ietf-aipref-attach-05` §3, which is the origin's and binds no +object (§4.4). + +Relationship to the core spec: `wai.ai.usage` is a **record** ([SPEC](../SPEC.md) §5): a +signed record a WAI tool emits and verifies. It names no payload format and is not +dispatchable. The manifest member `ai_usage` (§4.1) is an OPTIONAL member under SPEC §8's rule +for MINOR revisions. + +Relationship to [jwp-receipts](jwp-receipts.md): the envelope's statement is signed by the +object's receipt (§2.1 there); the consumption claim is a `claim` receipt of the class +`consumption` (§2.4.4 there), on that extension's envelope, canonical form, signature and +chain, with the body and class rules of §6 here. + +Relationship to [live-rooms](live-rooms.md): the room descriptor's layout is unchanged. A +room's statement is a record of its own, chained like the review policy of +[live-moderation](live-moderation.md) §3.4, and a grant states its principal's terms in the +attribute `ai_usage` (live-rooms §4.4). + +## 2. Terms + +- **Vocabulary**: one of `aipref`, `tdmrep` and `cawg`, the three sources §3 carries. +- **Term**: what a use is named by in a vocabulary: an `aipref` usage category label + (`train-ai`, `ai-use` or `search`, `draft-ietf-aipref-vocab-08` §6.1); the `tdmrep` property + `tdm-reservation`, which governs text and data mining as a whole; or a key of a + `cawg.training-mining` assertion's entries. +- **Preference**: what a vocabulary says about one term: **allow**, **disallow** or + **unknown** (`draft-ietf-aipref-vocab-08` §3). +- **Statement**: a JSON object of §3 holding one or more vocabularies. +- **Signed statement**: one of an envelope whose receipts verify (§4.1), or of a room usage + record that verifies (§4.2). +- **Agent**: a party that consumes media by automated means, for any use a vocabulary names. + An **agent principal** in a room is one whose grant carries the attribute `ai_usage` (§4.3). +- **Content hash**: an object's [jwp-receipts](jwp-receipts.md) §2.1 content hash, 32 bytes, + written as 64 lowercase hexadecimal digits in JSON. + +Integers are little-endian. A digest is SHA-256 (FIPS 180-4), 32 bytes. Ed25519 is RFC +8032's, verified strictly ([SPEC](../SPEC.md) §7 *Signatures*). The **canonical form** of a +JSON value is its JCS serialisation (RFC 8785) as jwp-receipts §2.4.3 restricts it: strings, +unsigned integers, `null`, arrays and objects. + +## 3. The usage statement + +### 3.1 Members + +A statement is a JSON object with one or more of these members and no other: + +| member | value | source | +|---|---|---| +| `aipref` | an object: `revision`, `value` | `draft-ietf-aipref-vocab-08` | +| `cawg` | an object: `revision`, `entries` | Training and Data Mining Assertion | +| `tdmrep` | an object: `revision`, `tdm-reservation`, `tdm-policy` | the TDM Reservation Protocol | + +Each vocabulary keeps its own terms and its own preferences. A statement maps no term of one +onto a term of another, and a WAI tool **MUST NOT** write a term none of them defines. + +### 3.2 `aipref` + +- `revision`: the string `draft-ietf-aipref-vocab-08`. +- `value`: a statement of preference in the vocabulary's exemplary serialisation + (`draft-ietf-aipref-vocab-08` §6), as the text an HTTP `Content-Usage` field would carry + (`draft-ietf-aipref-attach-05` §2): for example `train-ai=n, search=y`. It is 1 to 1024 + characters, each U+0000 to U+007F (§6.3 there: the syntax is ASCII), and it **MUST** parse + as a Dictionary (§3.3). + +The preference of a term is read as `draft-ietf-aipref-vocab-08` §6.5 has it: the Dictionary's +member whose key is the term's label, its parameters disregarded, gives **allow** when its value +is the Token `y`, **disallow** when it is the Token `n`, and **unknown** otherwise; a label +absent is unknown, and a key that is not one of the three labels is ignored (§6.4 there). When +a key repeats, the Dictionary keeps its last value (RFC 9651 §4.2.2), so `train-ai=n, train-ai=y` +allows. + +### 3.3 The Dictionary + +`value` parses as RFC 9651 §4.2 parses a field of type Dictionary: its characters are bytes +(§4.2 step 1, which fails on a byte above 0x7F); leading spaces are discarded; the Dictionary is +parsed by §4.2.2, with its keys (§4.2.3.3), parameters (§4.2.3.2), inner lists (§4.2.1.2) and +every bare item: Integer and Decimal (§4.2.4), String (§4.2.5), Token (§4.2.6), Byte Sequence +(§4.2.7), Boolean (§4.2.8), Date (§4.2.9) and Display String (§4.2.10); leading spaces are +discarded again; and any character left fails. RFC 9651 leaves two checks of a Byte Sequence +to the parser (§4.2.7); here the sequence parses when its content, the characters between its +colons, is characters of the base64 alphabet (RFC 4648 §4) followed by at most two `=`, with +the alphabet characters numbering other than one more than a multiple of four. Neither padding +nor pad bits are checked. + +`draft-ietf-aipref-vocab-08` §6.5.1 reads a value that does not parse as unknown for every +term. A statement does not carry one: a signer whose preference would be read as unknown +throughout has made a mistake that binding the statement would preserve, so it is refused. + +### 3.4 `cawg` + +- `revision`: the string `1.1`. +- `entries`: an object of 1 to 32 members, the assertion's `entries` map + (Training and Data Mining Assertion 1.1 §3.2), member for member. Each key is 1 to 255 bytes + of components separated by single `.`, a component being one or more of `a`–`z`, `A`–`Z`, + `0`–`9`, `_` and `-`; a key beginning `cawg.` is one of the four the assertion defines, + `cawg.data_mining`, `cawg.ai_inference`, `cawg.ai_generative_training` and `cawg.ai_training`, + since it reserves the prefix (§3.1 there). Each value is an object holding `use`, one of + `allowed`, `notAllowed` and `constrained`, and optionally `constraint_info`, 1 to 1024 bytes + of UTF-8 (the member the assertion's CDDL names; its prose spells it `constraints_info`). + +A term is a key: `allowed` is **allow**; `notAllowed` is **disallow**; `constrained` is +**disallow**, since the assertion treats it as `notAllowed` in the absence of additional +information (§3.1 there), which a grant can give (§5.2); a key absent is **unknown**. The +assertion's `metadata` map is not carried. + +### 3.5 `tdmrep` + +- `revision`: the string `CG-FINAL-tdmrep-20240510`. +- `tdm-reservation`: the integer 1, TDM rights are reserved, or 0, they are not (the + protocol's §5.1). The protocol treats any other value as unset; a statement carries none. +- `tdm-policy`: `null`, or the URL of the rightsholder's TDM policy (the protocol's §5.2): 8 to + 2048 bytes of `!` to `~` (0x21–0x7E), beginning `https://` or `http://`. + +The one term is `tdm-reservation`: 1 is **disallow** and 0 is **allow**. `tdm-policy` is +carried for whoever seeks a licence and is not evaluated. + +### 3.6 Reading a statement + +A statement is read from JSON in which no object names a member twice. It is refused at the +first of these that holds: + +1. It is not an object, it has no member, it has a member §3.1 does not list, or a + vocabulary's object has a member absent, a member its section does not list, or a member + of another type or outside its range: `usage_malformed`. A `revision` that is a string is + in range here; in `aipref`, a `value` that does not parse (§3.3) is not. +2. A vocabulary's `revision` is other than the string its section names: + `usage_revision_unknown`. +3. Its canonical form is longer than 4096 bytes: `usage_malformed`. + +So a statement with a malformed `tdmrep` and an unknown `aipref` revision is refused +`usage_malformed`. The **digest** of a statement is SHA-256 over its canonical form. + +## 4. Carriage + +### 4.1 The envelope: the manifest member `ai_usage` + +A `WAI1` or `WAI2` manifest ([SPEC](../SPEC.md) §3, §7.1) MAY carry, at its top level, the +member `ai_usage`, a statement. It applies to the object: every rendition and companion of a +`WAI2`. A reader of an earlier revision ignores it (SPEC §8); a malformed one leaves the +manifest readable, and this extension refuses the statement, not the envelope. + +The member is inside the envelope's bytes, and the object's content hash covers them +([jwp-receipts](jwp-receipts.md) §2.1): the group receipt that includes the object signs the +statement with the content. An object whose statement is altered, added or removed no longer +hashes to the content hash its receipt names, and fails its receipt. A verifier given an +envelope and its receipts: + +1. accepts the receipts as jwp-receipts §3 steps 1 to 5 do, the envelope hashing to the + object receipt's `content_hash` excepted; a refusal there is `receipt_invalid`; +2. requires the envelope to hash to that `content_hash`: otherwise `usage_unbound`; +3. reads the manifest's `ai_usage`, where present, by §3.6. + +An object that passes all three is **held**, with its statement **signed by** the group +receipt's `signer_pubkey`, or with none. A publisher that states a preference for an object writes the +member before it receipts the object. An envelope that carries no receipt carries an unsigned +statement, which §5.3 reads as it reads an HTTP field. + +### 4.2 A room: the room usage record `WAU1` + +The room authority states the room's preference in a **room usage record**: + +| field | size | meaning | +|---|---|---| +| `magic` | 4 | `WAU1` (ASCII) | +| `version` | 1 | 1 in this revision | +| `room` | 32 | the room id (live-rooms §3.2) | +| `prev` | 32 | the digest of the record this one replaces; 32 zero bytes for the first | +| `effective` | 8 | u64: the room-clock instant from which it applies; 0 for the first | +| `statement` | 2 + *n* | a u16 length, then the statement's canonical form, 2 to 4096 bytes | +| `sig` | 64 | Ed25519 by the room descriptor's `authority` over the 18 bytes `wai:ai-usage-room` (ASCII) and 0x01, followed by every byte from `magic` through `statement` | + +The record's **digest** is SHA-256 over the bytes from `magic` through `statement`. A reader +reads exactly the bytes it is given, in the order above, and refuses at the first of these that +holds: fewer than 4 bytes, `truncated`; a magic other than `WAU1`, `malformed`; no byte after +the magic, `truncated`; a version other than 1, `unknown_version`; the bytes end before a field, +`truncated`; bytes after `sig`, `malformed`. It then reads `statement` as JSON and by §3.6 +(`usage_malformed`, `usage_revision_unknown`), and refuses bytes that are not the canonical form +of what they hold (`usage_malformed`). Then `room` is the room id (`wrong_room`), and the +signature verifies under the authority (`bad_key`, `bad_signature`). + +A verifier is given a room's records in a list. The first has `prev` zero and `effective` 0; +each later one has as `prev` the digest of the one before it, and an `effective` above that +one's. A record that breaks this is refused `room_usage_unlinked`. The record **in effect** at +room-clock instant *t* is the last whose `effective` is at or below *t*. A record is replaced, +never amended: as `draft-ietf-aipref-attach-05` §3.3 has it for robots.txt, a change applies +from its instant on and never to what was published before. + +An object is the room's when it is given with the room (§8). It is governed at its manifest's +`live.t` ([live-ingest](live-ingest.md) §6), an integer; an object of the room whose manifest +has none is governed by every record in the list. + +### 4.3 A grant's terms: the attribute `ai_usage` + +A room grant MAY carry the attribute `ai_usage` (live-rooms §4.4), whose value is the canonical +form of a statement: the **terms** the authority grants that principal. They are an +arrangement in the sense of `draft-ietf-aipref-vocab-08` §5.2, which overrides a statement of +preference for the uses it names. A grant whose attribute does not read by §3.6, or is not the +canonical form of what it holds, is refused `grant_usage_malformed`, and its principal holds no +terms. The attribute is signed with the grant (live-rooms §4.2), and the 1024 bytes the grant's +attributes take bound the terms. + +### 4.4 HTTP: fields mapped from the signed statement + +An origin that serves an object ([http-delivery](http-delivery.md) §5) whose envelope carries +a statement **SHOULD** send, for a `GET` or `HEAD` of it: + +- for `aipref`: `Content-Usage:` and the statement's `value`, byte for byte + (`draft-ietf-aipref-attach-05` §2); +- for `tdmrep`: `tdm-reservation:` and `0` or `1`, and, where `tdm-policy` is not `null`, + `tdm-policy:` and the URL (the TDM Reservation Protocol §6.2). + +`cawg` has no HTTP field. These fields are unsigned. A client reads them as the response's +statement and compares it with the object's signed statement: + +- `Content-Usage` field lines are combined as RFC 9110 §5.3 combines them, with a comma and a + space, and parsed by §3.3. It **conflicts** when it does not parse, or when it gives any of + the three labels a preference other than the signed `value` gives it. Parameters, unknown keys + and spelling (`train-ai=n,search=y` against `train-ai=n, search=y`) do not conflict. +- `tdm-reservation` conflicts when more than one field line carries it, or when its value, + without leading and trailing spaces and tabs, is other than the signed one written in decimal. + `tdm-policy` conflicts when more than one field line carries it, or when its value, trimmed the + same way, is other than the signed URL, or the signed one is `null`. An absent field conflicts + with nothing. +- A field for a vocabulary the signed statement does not carry, or any field for an object with + no signed statement, conflicts with nothing: it is an unsigned statement (§5.3). + +A response whose fields conflict is refused `header_conflicts`. Field names compare without +regard to case. The robots.txt `Content-Usage` rule (`draft-ietf-aipref-attach-05` §3) states a +preference for a path, not for an object, and is unsigned; a WAI origin MAY write one, and this +extension reads none. + +### 4.5 MoQ: the catalog field `wai_ai_usage` + +A WAI track's catalog entry MAY carry `wai_ai_usage`, a statement +([moq-streaming-format](moq-streaming-format.md) §4), so that an agent knows the preference +before it subscribes. A catalog is unsigned. Its statement **MUST** be the statement of every +object on the track that carries one; a catalog whose statement does not read by §3.6, or whose +digest differs from the digest of a held object's statement on the track, is refused +`catalog_conflicts`. A room track's catalog **SHOULD** state the room record in effect when the +catalog is published. + +## 5. Precedence + +### 5.1 What governs an object + +An object's **governing statements** are its signed statements: the statement of its envelope, +when it is held (§4.1); and, when it is a room's, the room record in effect at its `live.t`, or +every record of the room where it has none (§4.2). A principal's **terms** are those of the grant +it acts under (§4.3). + +### 5.2 The rule + +The preference of a use, named by a term *T* of a vocabulary *V*, for an object and a principal: + +1. **Terms.** When the principal's terms carry *V* and give *T* allow or disallow, that is the + preference: an arrangement overrides a statement of preference + (`draft-ietf-aipref-vocab-08` §5.2). +2. **Signed statements.** Otherwise, of the governing statements that carry *V*: disallow when + any gives *T* disallow; else allow when any gives it allow; else unknown. This is the + vocabulary's own rule for combining statements (§5.1 there), the most restrictive applying. +3. **Unsigned statements.** When no governing statement carries *V*, an agent MAY read an + unsigned statement for *V* (an HTTP field, a catalog, an envelope without receipts) by the + same rule. It names no declaring party (§3.1 there), and no verifier can check what it said. + +A use named in several vocabularies is disallowed when any of them disallows it; allowed when +none disallows it and one allows it; unknown otherwise. A use that falls under no term of a +vocabulary is **outside** that vocabulary, which then says nothing about it. What an agent does +with an unknown preference is not this extension's: the vocabulary takes no position on a +default (§5 there), and a gate MAY (§7.1). + +### 5.3 Conflicts + +A signed statement governs over every unsigned one. An unsigned statement that differs from a +signed one is not the signer's: a client that finds one ignores it in step 3, reports the +response or catalog refused (`header_conflicts`, `catalog_conflicts`), and decides by the signed +statement. A stricter unsigned statement is the preference of a party nothing identifies; an +agent MAY honour it as well, and a consumption claim is checked against the signed statements +alone. Two signed statements that differ are not a conflict: rule 2 applies, and the most +restrictive governs. + +## 6. The consumption claim + +### 6.1 The body + +A consumption claim is a [jwp-receipts](jwp-receipts.md) §2.4.3 `claim` of `class` +`consumption`, `subject_type` `object`, signed by the agent's key, whose `subject` is the first +of `objects`. Its `signer_id` is the `did:key` of `signer_pubkey` (jwp-receipts §7.1), the key +that traces the agent. Its body has exactly these members, read in this order at jwp-receipts +§3.1 step 2: + +| member | type | meaning | +|---|---|---| +| `objects` | array of 32 B hex | the content hashes of the objects consumed, 1 to 1024 | +| `usage` | array of objects | the use, in each vocabulary the agent answers, 1 to 3 entries | +| `seen` | array of 32 B hex | the digests of the statements that governed the objects (§5.1), 0 to 64 | +| `grant` | 32 B hex or `null` | the digest of the room grant the agent consumed under (live-rooms §4.2) | + +A `usage` entry has exactly `vocabulary`, one of `aipref`, `cawg` and `tdmrep`, and `term`, a +string of 1 to 255 bytes or `null`. A term names the use as that vocabulary names it; `null` +states that the use falls under no term of the vocabulary. A value of `vocabulary` outside the +three is `field_type`. + +### 6.2 Class rules + +At jwp-receipts §3.1 step 4, in this order: + +1. `objects` is in strictly ascending order: `consumption_objects_unordered`. +2. `subject` is the first of `objects`: `consumption_subject_mismatch`. +3. `usage` is in strictly ascending order of `vocabulary`: `consumption_usage_unordered`. +4. Each `term` that is not `null` is a term of its vocabulary: for `aipref` one of `train-ai`, + `ai-use` and `search`; for `tdmrep`, `tdm-reservation`; for `cawg`, one of the assertion's + four keys or a key of §3.4's form that does not begin `cawg.`: + `consumption_term_unknown`. +5. `seen` is in strictly ascending order: `consumption_seen_unordered`. +6. `signer_id` is the `did:key` of `signer_pubkey`: `consumption_signer_not_principal`. + +### 6.3 Context + +At jwp-receipts §3.1 step 6 a verifier is given the objects it holds, with their receipts +(§4.1), and, where the claim names a grant or its objects are a room's, the room: its +descriptor, its grants and its usage records. It refuses at the first of these that fails: + +1. Each of `objects` is the content hash of an object it holds: `consumption_object_unknown`. +2. When `grant` is not `null`: it is the digest of one of the room's grants that live-rooms + §4.3 accepts, whose `principal` is `signer_pubkey`, and which is good at the `live.t` of + every object of the room the claim names that has one; and every object the claim names is + the room's. Otherwise `grant_mismatch`. A grant refused `grant_usage_malformed` holds no + terms and is still the principal's. +3. `seen` is exactly the digests of the objects' governing statements: `preference_mismatch`. +4. `usage` answers every vocabulary any governing statement carries: `usage_unanswered`. +5. For each object in the order of `objects`, and each entry in the order of `usage`, the + preference of §5.2, with the grant's terms where `grant` names one, is not disallow: + `usage_disallowed`. + +A room whose usage records are refused (§4.2) states no preference: its objects are governed +by their own statements alone. A grant the room refuses (live-rooms §4.3) is not one of its +grants, and a claim that names it is refused at step 2. + +### 6.4 What an accepted claim states + +An accepted claim states, under the agent's key, that the agent consumed those objects for that +use, having seen those statements, under that grant. A verifier reports, for each object, the +preference §5.2 gives the claimed use: **allowed**, **unknown**, or **outside** when every term +is `null`. A claim refused `usage_disallowed` is still its signer's statement: its signature and +chain hold, and what it records is a use the governing statements disallow. A refusal at step 6 +is of the use, never of the signature. The publisher, the agent and any party that holds the +objects and the claim reach the same verdict. + +## 7. The gate + +### 7.1 Policy + +A relay or a gateway MAY require consumption claims before it serves an agent. Whether it does, +and how it recognises an agent outside a room, is its own policy; in a room, an agent principal +is one whose grant carries terms (§4.3). The policy has two parameters: + +- `window`, 0 to 16: how many of the most recent groups the gate served the agent may be + unclaimed. With 0, every object the agent requests is claimed before it is served, as over + HTTP, where the agent names each object by its content hash before it fetches it + ([http-delivery](http-delivery.md) §5.2). With *n* ≥ 1, the agent claims what it received + after it received it, as over MOQT, where a live object's content hash is known only once it + is published. +- `require_allowed`: whether a use whose preference is unknown or outside is refused. + +### 7.2 Decision + +The gate holds the claims the agent gave it, each verified (§8) and signed by the agent's key +(in a room, its grant's principal; elsewhere, the key the gateway authenticates it by): an +accepted claim, or one refused `usage_disallowed`, is held; any other is not. It keeps the groups it served the agent, +in order, a group over HTTP being one object. For a request, a group of objects, the objects it +checks are the requested ones when `window` is 0, and otherwise those of every served group but +the last `window`. For each of those objects, in that order, it refuses at the first of these: + +1. No held claim names it: `claim_required`. +2. A held claim that names it was refused `usage_disallowed`: `usage_disallowed`. +3. With `require_allowed`, a held claim that names it gives it a preference other than + allowed: `usage_not_granted`. + +Otherwise it serves the request. A gate that refuses tells the agent the reason; over MOQT a +relay ends the subscription, and over HTTP a gateway answers `403`. + +### 7.3 Over HTTP + +An agent gives a gateway a claim with its request: the field `WAI-Usage-Claim:` and the claim's +canonical form in base64url without padding (RFC 4648 §5). The gateway verifies it, holds it as +§7.2 says, and decides. `WAI-Usage-Claim` is proposed for the HTTP Field Name Registry (RFC 9110 +§16.3), and is not registered. This revision defines no MOQT carriage for a claim; a relay that +gates over MOQT holds the claims its agents gave it over HTTP. + +## 8. Verification + +A verifier is given what each check reads, and nothing more: + +| check | inputs | outcome | +|---|---|---| +| a statement (§3.6) | its JSON | accepted with its digest and every term's preference, or refused | +| an object (§4.1) | its envelope, object receipt and group receipt | held, with its statement or none, or refused | +| a room (§4.2, §4.3) | its descriptor, grants and usage records | each record and grant accepted or refused | +| a response (§4.4) | an object and its response fields | consistent, or `header_conflicts` | +| a catalog (§4.5) | a catalog entry and the objects of its track | consistent, or refused | +| a claim (§6) | the claim, the claim before it of its signer, its objects, and its room | accepted with each object's preference, or refused at a step, with its reason | +| a gate (§7) | the policy, the served groups, the claims given, and a request | served, or refused with its reason | + +A room descriptor or a grant that does not verify is refused as live-rooms §3.4 and §4.3 +refuse it; a room with no verified descriptor governs nothing. + +## 9. Reasons + +The codes this extension adds, each with where it is raised: + +| reason | raised at | +|---|---| +| `usage_malformed` | §3.6, §4.2 | +| `usage_revision_unknown` | §3.6 | +| `receipt_invalid` | §4.1 step 1 | +| `usage_unbound` | §4.1 step 2 | +| `room_usage_unlinked` | §4.2 | +| `grant_usage_malformed` | §4.3 | +| `header_conflicts` | §4.4 | +| `catalog_conflicts` | §4.5 | +| `consumption_objects_unordered` | §6.2 rule 1 | +| `consumption_subject_mismatch` | §6.2 rule 2 | +| `consumption_usage_unordered` | §6.2 rule 3 | +| `consumption_term_unknown` | §6.2 rule 4 | +| `consumption_seen_unordered` | §6.2 rule 5 | +| `consumption_signer_not_principal` | §6.2 rule 6 | +| `consumption_object_unknown` | §6.3 step 1 | +| `grant_mismatch` | §6.3 step 2 | +| `preference_mismatch` | §6.3 step 3 | +| `usage_unanswered` | §6.3 step 4 | +| `usage_disallowed` | §6.3 step 5, §7.2 | +| `claim_required` | §7.2 | +| `usage_not_granted` | §7.2 | + +A room usage record also raises `truncated`, `malformed`, `unknown_version`, `wrong_room`, +`bad_key` and `bad_signature`, as live-rooms §3.4 names them, and a claim every code of +jwp-receipts §3.1 steps 1 to 5. + +## 10. Conformance + +[`live-conformance/usage/`](../live-conformance/usage/) holds the corpus, written from fixed +seeds by `wai_ai_usage_vectors gen` as `vectors.json`. It has: + +- **Statements**: each vocabulary alone and the three together, with every term's preference; + `aipref` values that exercise the Dictionary: a repeated key, parameters, a value that is not + a Token, an inner list, a key that is not a label, and every kind of bare item; and one + statement refused for each defect of §3.6, among them a value that does not parse and an + upper-case key. +- **Objects**: a publication whose envelopes carry statements under one group receipt, among + them a `WAI2`; the same envelope with its statement removed (`usage_unbound`), and altered + (`usage_unbound`); one under a receipt whose signature fails (`receipt_invalid`); and one whose + statement does not read. +- **Rooms**: a room with two chained usage records and the grants of two agent principals, one + of whose terms lift a disallow; a record of another room, one whose signature fails, one out of + its chain, and one whose statement does not read; a grant whose terms do not read. +- **Responses** and **catalogs**: fields that agree with the signed statement in another + spelling, and fields and a catalog that contradict it. +- **Claims**: claims accepted with each object's preference — allowed, unknown, outside, and + allowed under a grant over a room's disallow — and one refused for each reason of §6.2 and + §6.3, among them a use the statement forbids, objects the verifier does not hold, and a + signature that fails. +- **Gates**: requests served and refused under each policy, for each reason of §7.2. + +A conforming verifier gives every case the outcome, the reason, the step and everything else +the corpus states. `wai_ai_usage_vectors verify` and the test `tests/ai_usage.rs` check that the +file regenerates byte for byte, that `wai-rs` gives every case its outcome, that every reason of +§9 appears, and that §9 lists exactly the reference's codes. +`live-conformance/ai_usage_verify.py` is the second implementation, written from this text: it +shares no code with `wai-rs`; it reads rooms with the live-rooms corpus's second implementation +(`live_rooms_verify.py`), checks every signature with WAI's strict rule +(`claim-conformance/_ed25519.py`) and computes BLAKE3 with `claim-conformance/_blake3.py`, and +parses Structured Fields, canonical forms, receipts and claims itself; and it mutates copies of +the corpus and requires every copy to fail. + +## 11. Security and privacy considerations + +- A preference is not a security mechanism (`draft-ietf-aipref-vocab-08` §8). What this + extension adds is evidence: the publisher's statement bound to the bytes, and the agent's + statement of what it did, both signed. Neither makes a use happen or stops one. +- Removing an envelope's statement changes its content hash. An object served without its + receipts carries an unsigned statement at most, and an agent that requires receipts sees + the difference. A party that strips the statement and receipts the object under its own key + becomes that object's publisher, under its own key. +- An HTTP field and a catalog are written by whoever serves them. A signed statement governs + over both (§5.3), so an intermediary that rewrites a field changes nothing a claim is + checked against, and a client that compares them sees the rewrite. +- A consumption claim is signed by the agent's key and names the agent by that key alone + (jwp-receipts §7.1). A key-log confirmation, where the agent's origin publishes one, says + who holds the key; without one the claim is the key's statement. +- A claim lists the objects an agent consumed. A publisher that holds it learns what the agent + read and for what use, which is its purpose; an agent that claims under a gate discloses + that to the gate's operator too. +- A claim records the use its agent states. A false statement is a signed false statement: + a verifier checks it against the objects and the statements, and cannot check what the agent + did afterwards. +- `tdm-policy` is a URL a statement carries and a verifier never fetches. diff --git a/wai/live-conformance/usage/vectors.json b/wai/live-conformance/usage/vectors.json new file mode 100644 index 000000000..2743aa9d0 --- /dev/null +++ b/wai/live-conformance/usage/vectors.json @@ -0,0 +1,2202 @@ +{ + "extension": "ai-usage", + "revisions": { + "aipref": "draft-ietf-aipref-vocab-08", + "cawg": "1.1", + "tdmrep": "CG-FINAL-tdmrep-20240510" + }, + "keys": { + "publisher": "52ac4cd91fa02373db58946515d4b732915c07094d56031e48ce6df076db888f", + "authority": "4e3d5118d9b3a594d897ae1b59d10473343108338418359a1facb939992fec9f", + "clock": "24fcec79547e8a2bb76ddc969022a742556982d31d8171ef37ff43d0dba53f4e", + "presenter": "d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af", + "agent_a": "5599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf6567", + "agent_b": "84ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e9", + "crawler": "eeccba9faecd6f65af29ba7714605fe62d2195e6f031c7b8736f816d3eeeeb36", + "agent_d": "718b81b425ebfbd5c5da76f32d5550760bf023e24df858d2d9235a6e79cabfe9", + "stranger": "d54c21d1e62389f2f128767a6cdbaa320fa27fd9ba1f9463c3d92cb210d5054c" + }, + "statements": [ + { + "name": "aipref_only", + "description": "one vocabulary: training disallowed, search allowed, inference unknown", + "json": "{\"aipref\":{\"revision\":\"draft-ietf-aipref-vocab-08\",\"value\":\"train-ai=n, search=y\"}}", + "outcome": { + "digest": "03f90ce74223c9fe4513b6d702abc4fed6f8aaafca09f4b6de08d619b3fe0069", + "preferences": { + "aipref": { + "train-ai": "disallow", + "ai-use": "unknown", + "search": "allow" + } + }, + "outcome": "accept" + } + }, + { + "name": "three_vocabularies", + "description": "the three vocabularies together, in a member order the canonical form sorts", + "json": "{\"aipref\":{\"revision\":\"draft-ietf-aipref-vocab-08\",\"value\":\"train-ai=n, ai-use=y, search=y\"},\"cawg\":{\"revision\":\"1.1\",\"entries\":{\"cawg.ai_generative_training\":{\"use\":\"notAllowed\"},\"cawg.ai_inference\":{\"use\":\"allowed\"},\"cawg.data_mining\":{\"use\":\"constrained\",\"constraint_info\":\"licensed on request: https://licensing.example/tdm\"}}},\"tdmrep\":{\"revision\":\"CG-FINAL-tdmrep-20240510\",\"tdm-reservation\":1,\"tdm-policy\":\"https://licensing.example/tdm-policy.json\"}}", + "outcome": { + "digest": "1a55929b259aaf0c73c94a4f1ab932815ecd3c4a283c14ac3969b31568d52603", + "preferences": { + "aipref": { + "train-ai": "disallow", + "ai-use": "allow", + "search": "allow" + }, + "cawg": { + "cawg.ai_generative_training": "disallow", + "cawg.ai_inference": "allow", + "cawg.data_mining": "disallow" + }, + "tdmrep": { + "tdm-reservation": "disallow" + } + }, + "outcome": "accept" + } + }, + { + "name": "cawg_custom_key", + "description": "a key the assertion lets a claim generator add, and a predefined one", + "json": "{\"cawg\":{\"revision\":\"1.1\",\"entries\":{\"org.example.captioning\":{\"use\":\"allowed\"},\"cawg.ai_training\":{\"use\":\"notAllowed\"}}}}", + "outcome": { + "digest": "f1614feb6fbd885b706dd050eb56b4090a37404e73a0d9d354e10957d8ef456f", + "preferences": { + "cawg": { + "cawg.ai_training": "disallow", + "org.example.captioning": "allow" + } + }, + "outcome": "accept" + } + }, + { + "name": "tdmrep_not_reserved", + "description": "TDM rights not reserved, and no policy", + "json": "{\"tdmrep\":{\"revision\":\"CG-FINAL-tdmrep-20240510\",\"tdm-reservation\":0,\"tdm-policy\":null}}", + "outcome": { + "digest": "036472cb5ae99f6653ae18fbff5e3a8e657c84fa666161c5814b1846d7ebb8a6", + "preferences": { + "tdmrep": { + "tdm-reservation": "allow" + } + }, + "outcome": "accept" + } + }, + { + "name": "aipref_repeated_key", + "description": "a repeated key keeps its last value (RFC 9651 §4.2.2)", + "json": "{\"aipref\":{\"revision\":\"draft-ietf-aipref-vocab-08\",\"value\":\"train-ai=n, train-ai=y\"}}", + "outcome": { + "digest": "20213d966902c13c4e6ae7c513718a35aa1eb31d09259f8df63631c69bef0e8f", + "preferences": { + "aipref": { + "train-ai": "allow", + "ai-use": "unknown", + "search": "unknown" + } + }, + "outcome": "accept" + } + }, + { + "name": "aipref_parameters", + "description": "parameters are disregarded", + "json": "{\"aipref\":{\"revision\":\"draft-ietf-aipref-vocab-08\",\"value\":\"train-ai=y;source=\\\"publisher\\\", ai-use=n;q=0.5\"}}", + "outcome": { + "digest": "aa6b6a5f1c955e0a9aa1c6edbb1a7258fa6e7aa726d50986da5dafc58105ecdb", + "preferences": { + "aipref": { + "train-ai": "allow", + "ai-use": "disallow", + "search": "unknown" + } + }, + "outcome": "accept" + } + }, + { + "name": "aipref_not_tokens", + "description": "a String, a Boolean and an inner list are not the Token y or n: unknown", + "json": "{\"aipref\":{\"revision\":\"draft-ietf-aipref-vocab-08\",\"value\":\"train-ai=\\\"y\\\", ai-use=?1, search=(y n)\"}}", + "outcome": { + "digest": "d4005d5a4d264f721e5c31fb80cb4344ae2ed99c365f6de6f15abf146f862896", + "preferences": { + "aipref": { + "train-ai": "unknown", + "ai-use": "unknown", + "search": "unknown" + } + }, + "outcome": "accept" + } + }, + { + "name": "aipref_unknown_keys", + "description": "keys that are not labels are ignored; a key with no value is Boolean true", + "json": "{\"aipref\":{\"revision\":\"draft-ietf-aipref-vocab-08\",\"value\":\"foo=y, bar, *x=n, search=n\"}}", + "outcome": { + "digest": "1fe01a777f0cf82bd5dcf50041eccd74ad31664277bd5373fd49966f4612e283", + "preferences": { + "aipref": { + "train-ai": "unknown", + "ai-use": "unknown", + "search": "disallow" + } + }, + "outcome": "accept" + } + }, + { + "name": "aipref_every_bare_item", + "description": "every bare item type parses", + "json": "{\"aipref\":{\"revision\":\"draft-ietf-aipref-vocab-08\",\"value\":\"a=1, b=-2.5, c=\\\"s\\\\\\\"q\\\", d=tok/en:x, e=:aGk=:, f=?0, g=@1700000000, h=%\\\"caf%c3%a9\\\", i=(1 \\\"x\\\");p, train-ai=y\"}}", + "outcome": { + "digest": "af7113f7021fb73c082da97cd2df5b58ae47468d4c9786d1a597615f0e4b054a", + "preferences": { + "aipref": { + "train-ai": "allow", + "ai-use": "unknown", + "search": "unknown" + } + }, + "outcome": "accept" + } + }, + { + "name": "empty_object", + "description": "a statement with no member", + "json": "{}", + "outcome": { + "outcome": "refuse", + "reason": "usage_malformed" + } + }, + { + "name": "unknown_member", + "description": "a member §3.1 does not list", + "json": "{\"aipref\":{\"revision\":\"draft-ietf-aipref-vocab-08\",\"value\":\"train-ai=n\"},\"robots\":{}}", + "outcome": { + "outcome": "refuse", + "reason": "usage_malformed" + } + }, + { + "name": "aipref_double_comma", + "description": "a value that does not parse as a Dictionary", + "json": "{\"aipref\":{\"revision\":\"draft-ietf-aipref-vocab-08\",\"value\":\"train-ai=y,, search=n\"}}", + "outcome": { + "outcome": "refuse", + "reason": "usage_malformed" + } + }, + { + "name": "aipref_upper_case", + "description": "upper case fails the Dictionary (draft-ietf-aipref-vocab-08 §6.5.1)", + "json": "{\"aipref\":{\"revision\":\"draft-ietf-aipref-vocab-08\",\"value\":\"Train-AI=y\"}}", + "outcome": { + "outcome": "refuse", + "reason": "usage_malformed" + } + }, + { + "name": "aipref_trailing_comma", + "description": "a trailing comma", + "json": "{\"aipref\":{\"revision\":\"draft-ietf-aipref-vocab-08\",\"value\":\"train-ai=y,\"}}", + "outcome": { + "outcome": "refuse", + "reason": "usage_malformed" + } + }, + { + "name": "aipref_not_ascii", + "description": "a value that is not ASCII", + "json": "{\"aipref\":{\"revision\":\"draft-ietf-aipref-vocab-08\",\"value\":\"train-ai=é\"}}", + "outcome": { + "outcome": "refuse", + "reason": "usage_malformed" + } + }, + { + "name": "aipref_empty_value", + "description": "an empty value", + "json": "{\"aipref\":{\"revision\":\"draft-ietf-aipref-vocab-08\",\"value\":\"\"}}", + "outcome": { + "outcome": "refuse", + "reason": "usage_malformed" + } + }, + { + "name": "tdmrep_reservation_two", + "description": "a reservation other than 0 or 1", + "json": "{\"tdmrep\":{\"revision\":\"CG-FINAL-tdmrep-20240510\",\"tdm-reservation\":2,\"tdm-policy\":null}}", + "outcome": { + "outcome": "refuse", + "reason": "usage_malformed" + } + }, + { + "name": "tdmrep_policy_not_http", + "description": "a policy that is not an http or https URL", + "json": "{\"tdmrep\":{\"revision\":\"CG-FINAL-tdmrep-20240510\",\"tdm-reservation\":1,\"tdm-policy\":\"ftp://licensing.example/p\"}}", + "outcome": { + "outcome": "refuse", + "reason": "usage_malformed" + } + }, + { + "name": "tdmrep_policy_absent", + "description": "a vocabulary object missing a member", + "json": "{\"tdmrep\":{\"revision\":\"CG-FINAL-tdmrep-20240510\",\"tdm-reservation\":1}}", + "outcome": { + "outcome": "refuse", + "reason": "usage_malformed" + } + }, + { + "name": "cawg_reserved_prefix", + "description": "a cawg. key the assertion does not define", + "json": "{\"cawg\":{\"revision\":\"1.1\",\"entries\":{\"cawg.ai_summaries\":{\"use\":\"allowed\"}}}}", + "outcome": { + "outcome": "refuse", + "reason": "usage_malformed" + } + }, + { + "name": "cawg_use_unknown", + "description": "a use the assertion does not define", + "json": "{\"cawg\":{\"revision\":\"1.1\",\"entries\":{\"org.example.x\":{\"use\":\"maybe\"}}}}", + "outcome": { + "outcome": "refuse", + "reason": "usage_malformed" + } + }, + { + "name": "repeated_member", + "description": "a member named twice in one object", + "json": "{\"aipref\":{\"revision\":\"draft-ietf-aipref-vocab-08\",\"revision\":\"draft-ietf-aipref-vocab-08\",\"value\":\"train-ai=n\"}}", + "outcome": { + "outcome": "refuse", + "reason": "usage_malformed" + } + }, + { + "name": "canonical_too_long", + "description": "a canonical form longer than 4096 bytes", + "json": "{\"cawg\":{\"revision\":\"1.1\",\"entries\":{\"org.example.use00\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use01\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use02\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use03\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use04\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use05\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use06\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use07\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use08\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use09\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use10\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use11\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use12\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use13\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use14\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use15\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use16\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use17\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use18\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use19\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use20\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use21\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use22\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use23\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use24\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use25\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use26\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use27\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use28\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use29\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use30\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"},\"org.example.use31\":{\"use\":\"constrained\",\"constraint_info\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"}}}}", + "outcome": { + "outcome": "refuse", + "reason": "usage_malformed" + } + }, + { + "name": "aipref_revision_unknown", + "description": "a revision this extension does not read", + "json": "{\"aipref\":{\"revision\":\"draft-ietf-aipref-vocab-07\",\"value\":\"train-ai=n\"}}", + "outcome": { + "outcome": "refuse", + "reason": "usage_revision_unknown" + } + }, + { + "name": "cawg_revision_unknown", + "description": "the assertion's earlier version", + "json": "{\"cawg\":{\"revision\":\"1.0\",\"entries\":{\"cawg.ai_training\":{\"use\":\"allowed\"}}}}", + "outcome": { + "outcome": "refuse", + "reason": "usage_revision_unknown" + } + }, + { + "name": "malformed_before_revision", + "description": "a malformed vocabulary is refused before an unknown revision", + "json": "{\"aipref\":{\"revision\":\"draft-ietf-aipref-vocab-07\",\"value\":\"train-ai=n\"},\"tdmrep\":{\"revision\":\"CG-FINAL-tdmrep-20240510\",\"tdm-reservation\":2,\"tdm-policy\":null}}", + "outcome": { + "outcome": "refuse", + "reason": "usage_malformed" + } + } + ], + "publications": [ + { + "name": "catalogue", + "room": null, + "signer": "publisher", + "group": { + "kind": "group", + "track_alias": 1, + "group_id": 1, + "root_hash": "d586f37c3b062a2b674222631bd6cfe0f5974eb7eac06000b4f21ac699b3b17c", + "leaf_count": 4, + "joules_total": 0, + "group_start": "2026-10-21T09:00:00Z", + "group_end": "2026-10-21T09:00:10Z", + "signer_pubkey": "52ac4cd91fa02373db58946515d4b732915c07094d56031e48ce6df076db888f", + "signer_id": "did:key:z6Mkk1yrpopF7UXVQFhmq8MgvMuAtGuK71rHjw9heTxsgH4A", + "sig": "826d97ed5d9720cf44b73534c751a4ce1b5149c5ca3cdc3429c48ab72fc70a5956ec525b5047d3d72bdcc216e1c4536587609157058bf0bb3d318e6c730c8600", + "parent_receipt_hash": null + } + }, + { + "name": "unreadable", + "room": null, + "signer": "publisher", + "group": { + "kind": "group", + "track_alias": 2, + "group_id": 1, + "root_hash": "544c7d483ba3700573a95fcf09b9e4c6b37b9a5149982a173dd0f757ac756f21", + "leaf_count": 1, + "joules_total": 0, + "group_start": "2026-10-21T09:00:00Z", + "group_end": "2026-10-21T09:00:10Z", + "signer_pubkey": "52ac4cd91fa02373db58946515d4b732915c07094d56031e48ce6df076db888f", + "signer_id": "did:key:z6Mkk1yrpopF7UXVQFhmq8MgvMuAtGuK71rHjw9heTxsgH4A", + "sig": "a36e9960148f48bf0bd6703253af23bc933501468194c4cb300edb94004546cfec20fdc0f4e9d110e92259cc85fd7242a54e6bdcfb4238f211255d3e610c8000", + "parent_receipt_hash": null + } + }, + { + "name": "forged", + "room": null, + "signer": "publisher", + "group": { + "kind": "group", + "track_alias": 3, + "group_id": 1, + "root_hash": "b0ce49c11d3cfa590deae1742c2fb9b7dbdc31aed7978d486f3ae6520d81bd0f", + "leaf_count": 1, + "joules_total": 0, + "group_start": "2026-10-21T09:00:00Z", + "group_end": "2026-10-21T09:00:10Z", + "signer_pubkey": "52ac4cd91fa02373db58946515d4b732915c07094d56031e48ce6df076db888f", + "signer_id": "did:key:z6Mkk1yrpopF7UXVQFhmq8MgvMuAtGuK71rHjw9heTxsgH4A", + "sig": "f6acbada372e01e2c9fc2fe87ac1a346ded430c390178d96ecdd9a387f4e28d3fc47d556d76275858eac43f13da875a193501cc6729a2ac113960074b504ce07", + "parent_receipt_hash": null + } + }, + { + "name": "lecture_media", + "room": "lecture", + "signer": "presenter", + "group": { + "kind": "group", + "track_alias": 4, + "group_id": 1, + "root_hash": "85e7d7372f37888c5e3b071591165749c69c08f2489757fbe6e45ddcb8b833d2", + "leaf_count": 4, + "joules_total": 0, + "group_start": "2026-10-21T09:00:00Z", + "group_end": "2026-10-21T09:00:10Z", + "signer_pubkey": "d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af", + "signer_id": "did:key:z6Mkts6kqSLPycLErDdC9xcu8Bmtm43rczFk1sxDYxhWx7hY", + "sig": "b14299655c574a75dff7e26bcce2996050007cabca9e138e2417232f880f58a374bd7a953c59790ebe837bced87fccc75e18dd34a8d85b3896b4812eed70ec00", + "parent_receipt_hash": null + } + } + ], + "objects": [ + { + "name": "feature", + "description": "an object of its publication, under its own receipt", + "publication": "catalogue", + "envelope": "574149314b0200007b22776169223a22312e30222c226d65646961223a22766964656f222c22696e74656e74223a227265706c6963617465222c226d6f64656c5f726571756972656d656e74223a7b226361706162696c697479223a227761692e766964656f2e617631222c2266616c6c6261636b223a6e756c6c7d2c2261695f7573616765223a7b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d6e2c2061692d7573653d792c207365617263683d79227d2c2263617767223a7b22656e7472696573223a7b22636177672e61695f67656e657261746976655f747261696e696e67223a7b22757365223a226e6f74416c6c6f776564227d2c22636177672e61695f696e666572656e6365223a7b22757365223a22616c6c6f776564227d2c22636177672e646174615f6d696e696e67223a7b22636f6e73747261696e745f696e666f223a226c6963656e736564206f6e20726571756573743a2068747470733a2f2f6c6963656e73696e672e6578616d706c652f74646d222c22757365223a22636f6e73747261696e6564227d7d2c227265766973696f6e223a22312e31227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a2268747470733a2f2f6c6963656e73696e672e6578616d706c652f74646d2d706f6c6963792e6a736f6e222c2274646d2d7265736572766174696f6e223a317d7d7d1a000000666561747572653a206f70617175652074657374206279746573", + "receipt": { + "kind": "object", + "track_alias": 1, + "group_id": 1, + "object_id": 0, + "content_hash": "cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887", + "joules_micro": 0, + "origin": "", + "merkle_proof": { + "siblings": [ + "6b4eef144875caf7ec84e033ed436f7db2a0a3c2fd58cfd36ba0f3024e308020", + "c041e7632351002cb44d89a659de879fecfb14d746495c792214ea93eed972d5" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + "outcome": { + "outcome": "held", + "content_hash": "cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887", + "statement": "1a55929b259aaf0c73c94a4f1ab932815ecd3c4a283c14ac3969b31568d52603", + "t": null + } + }, + { + "name": "stills", + "description": "an object of its publication, under its own receipt", + "publication": "catalogue", + "envelope": "57414932730200007b22776169223a22312e31222c226d65646961223a22696d616765222c22696e74656e74223a227265706c6963617465222c2272656e646974696f6e73223a5b7b226361706162696c697479223a227761692e696d6167652e61766966222c226b696e64223a2261766966227d2c7b226361706162696c697479223a227761692e696d6167652e6a706567222c226b696e64223a226a706567227d5d2c2261695f7573616765223a7b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d6e2c2061692d7573653d792c207365617263683d79227d2c2263617767223a7b22656e7472696573223a7b22636177672e61695f67656e657261746976655f747261696e696e67223a7b22757365223a226e6f74416c6c6f776564227d2c22636177672e61695f696e666572656e6365223a7b22757365223a22616c6c6f776564227d2c22636177672e646174615f6d696e696e67223a7b22636f6e73747261696e745f696e666f223a226c6963656e736564206f6e20726571756573743a2068747470733a2f2f6c6963656e73696e672e6578616d706c652f74646d222c22757365223a22636f6e73747261696e6564227d7d2c227265766973696f6e223a22312e31227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a2268747470733a2f2f6c6963656e73696e672e6578616d706c652f74646d2d706f6c6963792e6a736f6e222c2274646d2d7265736572766174696f6e223a317d7d7d02000000000020000000200000002800000072656e646974696f6e206f6e653a206f7061717565207465737420627974657372656e646974696f6e2074776f3a206f706171756520746573742062797465732c206c6f6e676572", + "receipt": { + "kind": "object", + "track_alias": 1, + "group_id": 1, + "object_id": 1, + "content_hash": "3e0ea2c000e3b91fdc3705c72054c16ef27f008b23a9520104422fc887a6e2a4", + "joules_micro": 0, + "origin": "", + "merkle_proof": { + "siblings": [ + "06dcad741266c5e16712d31409349991d7e38a23b5bcddd1aff4af5638609696", + "c041e7632351002cb44d89a659de879fecfb14d746495c792214ea93eed972d5" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + "outcome": { + "outcome": "held", + "content_hash": "3e0ea2c000e3b91fdc3705c72054c16ef27f008b23a9520104422fc887a6e2a4", + "statement": "1a55929b259aaf0c73c94a4f1ab932815ecd3c4a283c14ac3969b31568d52603", + "t": null + } + }, + { + "name": "trailer", + "description": "an object of its publication, under its own receipt", + "publication": "catalogue", + "envelope": "57414931ca0000007b22776169223a22312e30222c226d65646961223a22766964656f222c22696e74656e74223a227265706c6963617465222c226d6f64656c5f726571756972656d656e74223a7b226361706162696c697479223a227761692e766964656f2e617631222c2266616c6c6261636b223a6e756c6c7d2c2261695f7573616765223a7b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d6e227d7d7d1a000000747261696c65723a206f70617175652074657374206279746573", + "receipt": { + "kind": "object", + "track_alias": 1, + "group_id": 1, + "object_id": 2, + "content_hash": "8208d92f31db13460a23915c20213038a8391714ceae054f0dd8c72ba9299350", + "joules_micro": 0, + "origin": "", + "merkle_proof": { + "siblings": [ + "4d29f8db47ee885a978efc607a4d12efe526d18b5bab7bd97e5e215334b073be", + "95328220a8c4792a5429fc933c9a78998f5324e607b6f3aaf91fc204c83367a5" + ], + "direction_bits": 2, + "leaf_index": 2 + } + }, + "outcome": { + "outcome": "held", + "content_hash": "8208d92f31db13460a23915c20213038a8391714ceae054f0dd8c72ba9299350", + "statement": "8135caccc8321524d0b2735fb8ba848b82826091303aa4fb46eba37088dd0159", + "t": null + } + }, + { + "name": "credits", + "description": "an object of its publication, under its own receipt", + "publication": "catalogue", + "envelope": "57414931750000007b22776169223a22312e30222c226d65646961223a22766964656f222c22696e74656e74223a227265706c6963617465222c226d6f64656c5f726571756972656d656e74223a7b226361706162696c697479223a227761692e766964656f2e617631222c2266616c6c6261636b223a6e756c6c7d7d1a000000637265646974733a206f70617175652074657374206279746573", + "receipt": { + "kind": "object", + "track_alias": 1, + "group_id": 1, + "object_id": 3, + "content_hash": "cf8bac17e7673c6b99f3854f4cc376ed1e168e47eae8f0e9316269122586e9d0", + "joules_micro": 0, + "origin": "", + "merkle_proof": { + "siblings": [ + "00bedbd5e62427bbd67f38447326c2ceedf97a9beb06b10afc936e4831b8e065", + "95328220a8c4792a5429fc933c9a78998f5324e607b6f3aaf91fc204c83367a5" + ], + "direction_bits": 3, + "leaf_index": 3 + } + }, + "outcome": { + "outcome": "held", + "content_hash": "cf8bac17e7673c6b99f3854f4cc376ed1e168e47eae8f0e9316269122586e9d0", + "statement": null, + "t": null + } + }, + { + "name": "reserved_twice", + "description": "an object of its publication, under its own receipt", + "publication": "unreadable", + "envelope": "57414931d90000007b22776169223a22312e30222c226d65646961223a22766964656f222c22696e74656e74223a227265706c6963617465222c226d6f64656c5f726571756972656d656e74223a7b226361706162696c697479223a227761692e766964656f2e617631222c2266616c6c6261636b223a6e756c6c7d2c2261695f7573616765223a7b2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a327d7d7d060000006f7061717565", + "receipt": { + "kind": "object", + "track_alias": 2, + "group_id": 1, + "object_id": 0, + "content_hash": "07bfdcea03c4d4ae2ac90540516d35d536395a120b3bc26e0d7e735b1c227623", + "joules_micro": 0, + "origin": "", + "merkle_proof": { + "siblings": [], + "direction_bits": 0, + "leaf_index": 0 + } + }, + "outcome": { + "outcome": "refuse", + "reason": "usage_malformed" + } + }, + { + "name": "signed_by_another", + "description": "an object of its publication, under its own receipt", + "publication": "forged", + "envelope": "574149314b0200007b22776169223a22312e30222c226d65646961223a22766964656f222c22696e74656e74223a227265706c6963617465222c226d6f64656c5f726571756972656d656e74223a7b226361706162696c697479223a227761692e766964656f2e617631222c2266616c6c6261636b223a6e756c6c7d2c2261695f7573616765223a7b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d6e2c2061692d7573653d792c207365617263683d79227d2c2263617767223a7b22656e7472696573223a7b22636177672e61695f67656e657261746976655f747261696e696e67223a7b22757365223a226e6f74416c6c6f776564227d2c22636177672e61695f696e666572656e6365223a7b22757365223a22616c6c6f776564227d2c22636177672e646174615f6d696e696e67223a7b22636f6e73747261696e745f696e666f223a226c6963656e736564206f6e20726571756573743a2068747470733a2f2f6c6963656e73696e672e6578616d706c652f74646d222c22757365223a22636f6e73747261696e6564227d7d2c227265766973696f6e223a22312e31227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a2268747470733a2f2f6c6963656e73696e672e6578616d706c652f74646d2d706f6c6963792e6a736f6e222c2274646d2d7265736572766174696f6e223a317d7d7d060000006f7061717565", + "receipt": { + "kind": "object", + "track_alias": 3, + "group_id": 1, + "object_id": 0, + "content_hash": "54b2dbfa9262d6ce04a5e72b8e48daf04a34c27eb2bcd2d697528da12e4aa8ae", + "joules_micro": 0, + "origin": "", + "merkle_proof": { + "siblings": [], + "direction_bits": 0, + "leaf_index": 0 + } + }, + "outcome": { + "outcome": "refuse", + "reason": "receipt_invalid" + } + }, + { + "name": "at_10s", + "description": "an object of its publication, under its own receipt", + "publication": "lecture_media", + "envelope": "57414931b00000007b22776169223a22312e30222c226d65646961223a22766964656f222c22696e74656e74223a227265706c6963617465222c226d6f64656c5f726571756972656d656e74223a7b226361706162696c697479223a227761692e766964656f2e617631222c2266616c6c6261636b223a6e756c6c7d2c226c697665223a7b2274223a31303030303030302c22727470223a3930303030302c2272617465223a39303030302c226b6579223a747275657d7d0f0000006c6563747572652061742031302073", + "receipt": { + "kind": "object", + "track_alias": 4, + "group_id": 1, + "object_id": 0, + "content_hash": "e09627f6f863adc91750c6193680047d1688f5f718aaed7e745334fe1c1712b0", + "joules_micro": 0, + "origin": "", + "merkle_proof": { + "siblings": [ + "3b4838efb03510f7b23a749d8290f91ada401e6742bcc34c33ff28f7fbb6cc33", + "1cb6b0457167ce394e3d96699d967651030386cb0967350166875c3d6d69b0fb" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + "outcome": { + "outcome": "held", + "content_hash": "e09627f6f863adc91750c6193680047d1688f5f718aaed7e745334fe1c1712b0", + "statement": null, + "t": 10000000 + } + }, + { + "name": "at_20s", + "description": "an object of its publication, under its own receipt", + "publication": "lecture_media", + "envelope": "57414931040100007b22776169223a22312e30222c226d65646961223a22766964656f222c22696e74656e74223a227265706c6963617465222c226d6f64656c5f726571756972656d656e74223a7b226361706162696c697479223a227761692e766964656f2e617631222c2266616c6c6261636b223a6e756c6c7d2c226c697665223a7b2274223a32303030303030302c22727470223a313830303030302c2272617465223a39303030302c226b6579223a747275657d2c2261695f7573616765223a7b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a227365617263683d79227d7d7d0f0000006c6563747572652061742032302073", + "receipt": { + "kind": "object", + "track_alias": 4, + "group_id": 1, + "object_id": 1, + "content_hash": "492bce59489cd76693ced443338c46a5ae22b0570af1887db34aa70e8ba28a95", + "joules_micro": 0, + "origin": "", + "merkle_proof": { + "siblings": [ + "c84ae31db8d839666107422ca864791f1d7430c32c8c77856f90acd8f1dd7ee6", + "1cb6b0457167ce394e3d96699d967651030386cb0967350166875c3d6d69b0fb" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + "outcome": { + "outcome": "held", + "content_hash": "492bce59489cd76693ced443338c46a5ae22b0570af1887db34aa70e8ba28a95", + "statement": "d186796906e025546774b20e0aaa936e929d7e1702faefe764d03e34ffe0eef5", + "t": 20000000 + } + }, + { + "name": "at_70s", + "description": "an object of its publication, under its own receipt", + "publication": "lecture_media", + "envelope": "57414931b10000007b22776169223a22312e30222c226d65646961223a22766964656f222c22696e74656e74223a227265706c6963617465222c226d6f64656c5f726571756972656d656e74223a7b226361706162696c697479223a227761692e766964656f2e617631222c2266616c6c6261636b223a6e756c6c7d2c226c697665223a7b2274223a37303030303030302c22727470223a363330303030302c2272617465223a39303030302c226b6579223a747275657d7d0f0000006c6563747572652061742037302073", + "receipt": { + "kind": "object", + "track_alias": 4, + "group_id": 1, + "object_id": 2, + "content_hash": "6ee066b579606409d041380760beff71e7f1589d838c270afd0781c1db377fd6", + "joules_micro": 0, + "origin": "", + "merkle_proof": { + "siblings": [ + "9b075fff09e63bd281bc4278f9f8eee032269eac7b87d31581a34ed9926c558d", + "e28ec0f9422ab2325e998c00a0f8d862a6cedbe5b5524621dc6ce142672d0930" + ], + "direction_bits": 2, + "leaf_index": 2 + } + }, + "outcome": { + "outcome": "held", + "content_hash": "6ee066b579606409d041380760beff71e7f1589d838c270afd0781c1db377fd6", + "statement": null, + "t": 70000000 + } + }, + { + "name": "untimed", + "description": "an object of its publication, under its own receipt", + "publication": "lecture_media", + "envelope": "57414931750000007b22776169223a22312e30222c226d65646961223a22766964656f222c22696e74656e74223a227265706c6963617465222c226d6f64656c5f726571756972656d656e74223a7b226361706162696c697479223a227761692e766964656f2e617631222c2266616c6c6261636b223a6e756c6c7d7d100000006c6563747572652c20756e74696d6564", + "receipt": { + "kind": "object", + "track_alias": 4, + "group_id": 1, + "object_id": 3, + "content_hash": "18b622c8e2b90b10d6b9af355467881354d1cf3a6a4e2b10f8ea6be3e0946f2b", + "joules_micro": 0, + "origin": "", + "merkle_proof": { + "siblings": [ + "4d0e4c80852c171f079374ba0bd6a303aa2bad2060e98d75c626c87c6f8c3bd9", + "e28ec0f9422ab2325e998c00a0f8d862a6cedbe5b5524621dc6ce142672d0930" + ], + "direction_bits": 3, + "leaf_index": 3 + } + }, + "outcome": { + "outcome": "held", + "content_hash": "18b622c8e2b90b10d6b9af355467881354d1cf3a6a4e2b10f8ea6be3e0946f2b", + "statement": null, + "t": null + } + }, + { + "name": "feature_stripped", + "description": "the feature with its statement removed, under the feature's receipt", + "publication": "catalogue", + "envelope": "57414931750000007b22776169223a22312e30222c226d65646961223a22766964656f222c22696e74656e74223a227265706c6963617465222c226d6f64656c5f726571756972656d656e74223a7b226361706162696c697479223a227761692e766964656f2e617631222c2266616c6c6261636b223a6e756c6c7d7d1a000000666561747572653a206f70617175652074657374206279746573", + "receipt": { + "kind": "object", + "track_alias": 1, + "group_id": 1, + "object_id": 0, + "content_hash": "cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887", + "joules_micro": 0, + "origin": "", + "merkle_proof": { + "siblings": [ + "6b4eef144875caf7ec84e033ed436f7db2a0a3c2fd58cfd36ba0f3024e308020", + "c041e7632351002cb44d89a659de879fecfb14d746495c792214ea93eed972d5" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + "outcome": { + "outcome": "refuse", + "reason": "usage_unbound" + } + }, + { + "name": "feature_altered", + "description": "the feature with training allowed in its statement, under the feature's receipt", + "publication": "catalogue", + "envelope": "574149314b0200007b22776169223a22312e30222c226d65646961223a22766964656f222c22696e74656e74223a227265706c6963617465222c226d6f64656c5f726571756972656d656e74223a7b226361706162696c697479223a227761692e766964656f2e617631222c2266616c6c6261636b223a6e756c6c7d2c2261695f7573616765223a7b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d792c2061692d7573653d792c207365617263683d79227d2c2263617767223a7b22656e7472696573223a7b22636177672e61695f67656e657261746976655f747261696e696e67223a7b22757365223a226e6f74416c6c6f776564227d2c22636177672e61695f696e666572656e6365223a7b22757365223a22616c6c6f776564227d2c22636177672e646174615f6d696e696e67223a7b22636f6e73747261696e745f696e666f223a226c6963656e736564206f6e20726571756573743a2068747470733a2f2f6c6963656e73696e672e6578616d706c652f74646d222c22757365223a22636f6e73747261696e6564227d7d2c227265766973696f6e223a22312e31227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a2268747470733a2f2f6c6963656e73696e672e6578616d706c652f74646d2d706f6c6963792e6a736f6e222c2274646d2d7265736572766174696f6e223a317d7d7d1a000000666561747572653a206f70617175652074657374206279746573", + "receipt": { + "kind": "object", + "track_alias": 1, + "group_id": 1, + "object_id": 0, + "content_hash": "cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887", + "joules_micro": 0, + "origin": "", + "merkle_proof": { + "siblings": [ + "6b4eef144875caf7ec84e033ed436f7db2a0a3c2fd58cfd36ba0f3024e308020", + "c041e7632351002cb44d89a659de879fecfb14d746495c792214ea93eed972d5" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + "outcome": { + "outcome": "refuse", + "reason": "usage_unbound" + } + } + ], + "rooms": [ + { + "name": "lecture", + "description": "a room with two chained usage records and two agent principals, one of whose terms lift a disallow", + "descriptor": "57524431014e3d5118d9b3a594d897ae1b59d10473343108338418359a1facb939992fec9f24fcec79547e8a2bb76ddc969022a742556982d31d8171ef37ff43d0dba53f4e0020a16d4e5e060040420f00000000000000100000000200000000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000020970726573656e746572020101000000000000000000000000000000000000000000000000000000000000000000056167656e740001010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ecb5997cca9e941c7649e75bae5f8819248af8bff8b31bb56e8e69d47a6a3b11179394006d85f666de998d9175985b7ae7d3c613731c84d0696e4268bb90901", + "grants": [ + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c1d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af0000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000002010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c650000d2bb4d9f2bf3c4983bb769e2a76a6c402ad8067dfce4ea3222160fa161de3b6cfe058b530e006bc60f21de7004a61678d12b2bb9ba53609df31c489cd123e40e", + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c15599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf65670000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000100010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c6552000861695f757361676547007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a2261692d7573653d79227d7d38ce334dc24b7d6f2e7741f31af5656fdc808b9f86f8b2a260267443e844fde3fe13caaba3d5db439728c06d7563f2793f006b659aab4ab59ee3871f47ca5f0e", + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c184ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e90000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000100010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c65b5000861695f7573616765aa007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d792c2061692d7573653d79227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a307d7d59308bc2f1cc32bf9f2d6a2dd2147970988903a0e088ddfc7da06e6c9378099f3df134fa310f46010ecd3bff8e0b71fc32a9bc74082b373208574400c5a8d503" + ], + "records": [ + "5741553101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c100000000000000000000000000000000000000000000000000000000000000000000000000000000b4007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d6e2c2061692d7573653d792c207365617263683d6e227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a317d7d690f7ce597254323f05ee0cc8ae599dcfa4d895fb95d7273ab66459a17e5db8419366eb9e3a5688d9ab074f8b3ef0c43387e3a161e29d56311b93d233a5e1e0d", + "5741553101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c1cd6140e57906f518cab1e2ffffad0b008c199658337d1f016d8d2893d19d08a60087930300000000b4007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d6e2c2061692d7573653d6e2c207365617263683d6e227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a317d7db6e906d858eb9a5c45c5770ad433731938d702ce1465dfb43bb62b66d90d6a8015da4ed463781c80db5a60e9ef0db84269d66184e63e47a8b19dab13ebf75207" + ], + "outcome": { + "grants": "accept", + "records": "accept", + "terms": [ + { + "grant": "4b08089d169245776c7448605525fafbe3d5b810bd4fa0560c0b6c7873695d60", + "principal": "d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af", + "terms": null + }, + { + "grant": "5aaa435d2ebbfa1f014389c89737723314b85cc75276ef2dd233046e2be3bcc6", + "principal": "5599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf6567", + "terms": "8f22c1a79f254ccdfe1fdb9079024f9d6d9587c8ae0cfd1e34fcf092be1ee9a1" + }, + { + "grant": "91e8b7e1b875f610644a02af8e8fcfba174083546780f10240cd5bf3a0497e37", + "principal": "84ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e9", + "terms": "2df3d2b9315e76d2258ddcbfa338aa3bdf0657f399495027d8cc5bc40b3d0417" + } + ], + "in_effect": [ + { + "t": 0, + "statements": [ + "033e03c9f353abb164f64ed3c147a8b4c2a7460f51376545e398e4c652f07908" + ] + }, + { + "t": 59000000, + "statements": [ + "033e03c9f353abb164f64ed3c147a8b4c2a7460f51376545e398e4c652f07908" + ] + }, + { + "t": 60000000, + "statements": [ + "3a7d7c0d1a6fdd2c35080206d58914c989a9de0ecd75d78b70b836361ad10960" + ] + }, + { + "t": 3600000000, + "statements": [ + "3a7d7c0d1a6fdd2c35080206d58914c989a9de0ecd75d78b70b836361ad10960" + ] + }, + { + "t": null, + "statements": [ + "033e03c9f353abb164f64ed3c147a8b4c2a7460f51376545e398e4c652f07908", + "3a7d7c0d1a6fdd2c35080206d58914c989a9de0ecd75d78b70b836361ad10960" + ] + } + ] + } + }, + { + "name": "record_of_another_room", + "description": "a record naming another room", + "descriptor": "57524431014e3d5118d9b3a594d897ae1b59d10473343108338418359a1facb939992fec9f24fcec79547e8a2bb76ddc969022a742556982d31d8171ef37ff43d0dba53f4e0020a16d4e5e060040420f00000000000000100000000200000000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000020970726573656e746572020101000000000000000000000000000000000000000000000000000000000000000000056167656e740001010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ecb5997cca9e941c7649e75bae5f8819248af8bff8b31bb56e8e69d47a6a3b11179394006d85f666de998d9175985b7ae7d3c613731c84d0696e4268bb90901", + "grants": [ + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c1d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af0000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000002010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c650000d2bb4d9f2bf3c4983bb769e2a76a6c402ad8067dfce4ea3222160fa161de3b6cfe058b530e006bc60f21de7004a61678d12b2bb9ba53609df31c489cd123e40e", + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c15599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf65670000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000100010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c6552000861695f757361676547007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a2261692d7573653d79227d7d38ce334dc24b7d6f2e7741f31af5656fdc808b9f86f8b2a260267443e844fde3fe13caaba3d5db439728c06d7563f2793f006b659aab4ab59ee3871f47ca5f0e", + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c184ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e90000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000100010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c65b5000861695f7573616765aa007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d792c2061692d7573653d79227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a307d7d59308bc2f1cc32bf9f2d6a2dd2147970988903a0e088ddfc7da06e6c9378099f3df134fa310f46010ecd3bff8e0b71fc32a9bc74082b373208574400c5a8d503" + ], + "records": [ + "57415531013b8deed48751f5b0b800e9602dc35e5892d855cf03501d26caccbbbb18b253ad00000000000000000000000000000000000000000000000000000000000000000000000000000000b4007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d6e2c2061692d7573653d792c207365617263683d6e227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a317d7d07f41d1edff7b3cc0909e87a6ffa8e8ef2fafb73ffed37f083737d3fd76a3ce2e28c1ccb45090c7b2ed8d6e8d6362bb7553c60caa30ddde4a43475e77a0fc20e" + ], + "outcome": { + "grants": "accept", + "records": { + "index": 0, + "reason": "wrong_room" + }, + "terms": [ + { + "grant": "4b08089d169245776c7448605525fafbe3d5b810bd4fa0560c0b6c7873695d60", + "principal": "d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af", + "terms": null + }, + { + "grant": "5aaa435d2ebbfa1f014389c89737723314b85cc75276ef2dd233046e2be3bcc6", + "principal": "5599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf6567", + "terms": "8f22c1a79f254ccdfe1fdb9079024f9d6d9587c8ae0cfd1e34fcf092be1ee9a1" + }, + { + "grant": "91e8b7e1b875f610644a02af8e8fcfba174083546780f10240cd5bf3a0497e37", + "principal": "84ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e9", + "terms": "2df3d2b9315e76d2258ddcbfa338aa3bdf0657f399495027d8cc5bc40b3d0417" + } + ] + } + }, + { + "name": "record_not_the_authoritys", + "description": "a record signed by a key other than the authority", + "descriptor": "57524431014e3d5118d9b3a594d897ae1b59d10473343108338418359a1facb939992fec9f24fcec79547e8a2bb76ddc969022a742556982d31d8171ef37ff43d0dba53f4e0020a16d4e5e060040420f00000000000000100000000200000000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000020970726573656e746572020101000000000000000000000000000000000000000000000000000000000000000000056167656e740001010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ecb5997cca9e941c7649e75bae5f8819248af8bff8b31bb56e8e69d47a6a3b11179394006d85f666de998d9175985b7ae7d3c613731c84d0696e4268bb90901", + "grants": [ + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c1d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af0000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000002010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c650000d2bb4d9f2bf3c4983bb769e2a76a6c402ad8067dfce4ea3222160fa161de3b6cfe058b530e006bc60f21de7004a61678d12b2bb9ba53609df31c489cd123e40e", + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c15599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf65670000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000100010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c6552000861695f757361676547007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a2261692d7573653d79227d7d38ce334dc24b7d6f2e7741f31af5656fdc808b9f86f8b2a260267443e844fde3fe13caaba3d5db439728c06d7563f2793f006b659aab4ab59ee3871f47ca5f0e", + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c184ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e90000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000100010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c65b5000861695f7573616765aa007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d792c2061692d7573653d79227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a307d7d59308bc2f1cc32bf9f2d6a2dd2147970988903a0e088ddfc7da06e6c9378099f3df134fa310f46010ecd3bff8e0b71fc32a9bc74082b373208574400c5a8d503" + ], + "records": [ + "5741553101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c100000000000000000000000000000000000000000000000000000000000000000000000000000000b4007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d6e2c2061692d7573653d792c207365617263683d6e227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a317d7d43d662feb2427f6827f6c6145d77da1e3ed29aafb954cc8411d86e9ff29a36b6c32bb59a9cd1dae4017e063da849fa5d5431d158d9ee4210155f452de714b808" + ], + "outcome": { + "grants": "accept", + "records": { + "index": 0, + "reason": "bad_signature" + }, + "terms": [ + { + "grant": "4b08089d169245776c7448605525fafbe3d5b810bd4fa0560c0b6c7873695d60", + "principal": "d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af", + "terms": null + }, + { + "grant": "5aaa435d2ebbfa1f014389c89737723314b85cc75276ef2dd233046e2be3bcc6", + "principal": "5599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf6567", + "terms": "8f22c1a79f254ccdfe1fdb9079024f9d6d9587c8ae0cfd1e34fcf092be1ee9a1" + }, + { + "grant": "91e8b7e1b875f610644a02af8e8fcfba174083546780f10240cd5bf3a0497e37", + "principal": "84ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e9", + "terms": "2df3d2b9315e76d2258ddcbfa338aa3bdf0657f399495027d8cc5bc40b3d0417" + } + ] + } + }, + { + "name": "record_unlinked", + "description": "a second record that does not name the first", + "descriptor": "57524431014e3d5118d9b3a594d897ae1b59d10473343108338418359a1facb939992fec9f24fcec79547e8a2bb76ddc969022a742556982d31d8171ef37ff43d0dba53f4e0020a16d4e5e060040420f00000000000000100000000200000000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000020970726573656e746572020101000000000000000000000000000000000000000000000000000000000000000000056167656e740001010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ecb5997cca9e941c7649e75bae5f8819248af8bff8b31bb56e8e69d47a6a3b11179394006d85f666de998d9175985b7ae7d3c613731c84d0696e4268bb90901", + "grants": [ + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c1d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af0000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000002010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c650000d2bb4d9f2bf3c4983bb769e2a76a6c402ad8067dfce4ea3222160fa161de3b6cfe058b530e006bc60f21de7004a61678d12b2bb9ba53609df31c489cd123e40e", + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c15599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf65670000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000100010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c6552000861695f757361676547007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a2261692d7573653d79227d7d38ce334dc24b7d6f2e7741f31af5656fdc808b9f86f8b2a260267443e844fde3fe13caaba3d5db439728c06d7563f2793f006b659aab4ab59ee3871f47ca5f0e", + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c184ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e90000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000100010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c65b5000861695f7573616765aa007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d792c2061692d7573653d79227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a307d7d59308bc2f1cc32bf9f2d6a2dd2147970988903a0e088ddfc7da06e6c9378099f3df134fa310f46010ecd3bff8e0b71fc32a9bc74082b373208574400c5a8d503" + ], + "records": [ + "5741553101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c100000000000000000000000000000000000000000000000000000000000000000000000000000000b4007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d6e2c2061692d7573653d792c207365617263683d6e227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a317d7d690f7ce597254323f05ee0cc8ae599dcfa4d895fb95d7273ab66459a17e5db8419366eb9e3a5688d9ab074f8b3ef0c43387e3a161e29d56311b93d233a5e1e0d", + "5741553101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c100000000000000000000000000000000000000000000000000000000000000000087930300000000b4007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d6e2c2061692d7573653d6e2c207365617263683d6e227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a317d7d4ff4bb0a2ea2d7bf75f394f6fe000831cf2712404b47c9b577e5518cfafc1abe28349dbd924dcceb43e567fd12b704f00191e3923a2e6c666aa3ca806a07bb0a" + ], + "outcome": { + "grants": "accept", + "records": { + "index": 1, + "reason": "room_usage_unlinked" + }, + "terms": [ + { + "grant": "4b08089d169245776c7448605525fafbe3d5b810bd4fa0560c0b6c7873695d60", + "principal": "d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af", + "terms": null + }, + { + "grant": "5aaa435d2ebbfa1f014389c89737723314b85cc75276ef2dd233046e2be3bcc6", + "principal": "5599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf6567", + "terms": "8f22c1a79f254ccdfe1fdb9079024f9d6d9587c8ae0cfd1e34fcf092be1ee9a1" + }, + { + "grant": "91e8b7e1b875f610644a02af8e8fcfba174083546780f10240cd5bf3a0497e37", + "principal": "84ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e9", + "terms": "2df3d2b9315e76d2258ddcbfa338aa3bdf0657f399495027d8cc5bc40b3d0417" + } + ] + } + }, + { + "name": "record_not_later", + "description": "a second record that takes effect no later than the first", + "descriptor": "57524431014e3d5118d9b3a594d897ae1b59d10473343108338418359a1facb939992fec9f24fcec79547e8a2bb76ddc969022a742556982d31d8171ef37ff43d0dba53f4e0020a16d4e5e060040420f00000000000000100000000200000000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000020970726573656e746572020101000000000000000000000000000000000000000000000000000000000000000000056167656e740001010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ecb5997cca9e941c7649e75bae5f8819248af8bff8b31bb56e8e69d47a6a3b11179394006d85f666de998d9175985b7ae7d3c613731c84d0696e4268bb90901", + "grants": [ + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c1d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af0000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000002010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c650000d2bb4d9f2bf3c4983bb769e2a76a6c402ad8067dfce4ea3222160fa161de3b6cfe058b530e006bc60f21de7004a61678d12b2bb9ba53609df31c489cd123e40e", + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c15599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf65670000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000100010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c6552000861695f757361676547007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a2261692d7573653d79227d7d38ce334dc24b7d6f2e7741f31af5656fdc808b9f86f8b2a260267443e844fde3fe13caaba3d5db439728c06d7563f2793f006b659aab4ab59ee3871f47ca5f0e", + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c184ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e90000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000100010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c65b5000861695f7573616765aa007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d792c2061692d7573653d79227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a307d7d59308bc2f1cc32bf9f2d6a2dd2147970988903a0e088ddfc7da06e6c9378099f3df134fa310f46010ecd3bff8e0b71fc32a9bc74082b373208574400c5a8d503" + ], + "records": [ + "5741553101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c100000000000000000000000000000000000000000000000000000000000000000000000000000000b4007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d6e2c2061692d7573653d792c207365617263683d6e227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a317d7d690f7ce597254323f05ee0cc8ae599dcfa4d895fb95d7273ab66459a17e5db8419366eb9e3a5688d9ab074f8b3ef0c43387e3a161e29d56311b93d233a5e1e0d", + "5741553101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c1cd6140e57906f518cab1e2ffffad0b008c199658337d1f016d8d2893d19d08a60000000000000000b4007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d6e2c2061692d7573653d6e2c207365617263683d6e227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a317d7d331c1fcd5d22c2409664caa6fd9a44f7c8386f146d0e1858e507af1a07a44785eb284992724f96f993875699213a5017c01d75bafb9fc05095ca4ce27ec61000" + ], + "outcome": { + "grants": "accept", + "records": { + "index": 1, + "reason": "room_usage_unlinked" + }, + "terms": [ + { + "grant": "4b08089d169245776c7448605525fafbe3d5b810bd4fa0560c0b6c7873695d60", + "principal": "d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af", + "terms": null + }, + { + "grant": "5aaa435d2ebbfa1f014389c89737723314b85cc75276ef2dd233046e2be3bcc6", + "principal": "5599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf6567", + "terms": "8f22c1a79f254ccdfe1fdb9079024f9d6d9587c8ae0cfd1e34fcf092be1ee9a1" + }, + { + "grant": "91e8b7e1b875f610644a02af8e8fcfba174083546780f10240cd5bf3a0497e37", + "principal": "84ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e9", + "terms": "2df3d2b9315e76d2258ddcbfa338aa3bdf0657f399495027d8cc5bc40b3d0417" + } + ] + } + }, + { + "name": "record_not_canonical", + "description": "a record whose statement bytes are not the canonical form", + "descriptor": "57524431014e3d5118d9b3a594d897ae1b59d10473343108338418359a1facb939992fec9f24fcec79547e8a2bb76ddc969022a742556982d31d8171ef37ff43d0dba53f4e0020a16d4e5e060040420f00000000000000100000000200000000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000020970726573656e746572020101000000000000000000000000000000000000000000000000000000000000000000056167656e740001010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ecb5997cca9e941c7649e75bae5f8819248af8bff8b31bb56e8e69d47a6a3b11179394006d85f666de998d9175985b7ae7d3c613731c84d0696e4268bb90901", + "grants": [ + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c1d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af0000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000002010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c650000d2bb4d9f2bf3c4983bb769e2a76a6c402ad8067dfce4ea3222160fa161de3b6cfe058b530e006bc60f21de7004a61678d12b2bb9ba53609df31c489cd123e40e", + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c15599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf65670000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000100010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c6552000861695f757361676547007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a2261692d7573653d79227d7d38ce334dc24b7d6f2e7741f31af5656fdc808b9f86f8b2a260267443e844fde3fe13caaba3d5db439728c06d7563f2793f006b659aab4ab59ee3871f47ca5f0e", + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c184ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e90000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000100010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c65b5000861695f7573616765aa007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d792c2061692d7573653d79227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a307d7d59308bc2f1cc32bf9f2d6a2dd2147970988903a0e088ddfc7da06e6c9378099f3df134fa310f46010ecd3bff8e0b71fc32a9bc74082b373208574400c5a8d503" + ], + "records": [ + "5741553101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c100000000000000000000000000000000000000000000000000000000000000000000000000000000b500207b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d6e2c2061692d7573653d792c207365617263683d6e227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a317d7d0f79943943b4eff0173c9fb229c3e720f370d612e4a2415832d8754f4cacf6709f12c2fac5ed66cd9293eaac1fcc4d850c63ffd68ed126d0bce5e01aef08ae0b" + ], + "outcome": { + "grants": "accept", + "records": { + "index": 0, + "reason": "usage_malformed" + }, + "terms": [ + { + "grant": "4b08089d169245776c7448605525fafbe3d5b810bd4fa0560c0b6c7873695d60", + "principal": "d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af", + "terms": null + }, + { + "grant": "5aaa435d2ebbfa1f014389c89737723314b85cc75276ef2dd233046e2be3bcc6", + "principal": "5599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf6567", + "terms": "8f22c1a79f254ccdfe1fdb9079024f9d6d9587c8ae0cfd1e34fcf092be1ee9a1" + }, + { + "grant": "91e8b7e1b875f610644a02af8e8fcfba174083546780f10240cd5bf3a0497e37", + "principal": "84ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e9", + "terms": "2df3d2b9315e76d2258ddcbfa338aa3bdf0657f399495027d8cc5bc40b3d0417" + } + ] + } + }, + { + "name": "record_statement_malformed", + "description": "a record whose statement does not read", + "descriptor": "57524431014e3d5118d9b3a594d897ae1b59d10473343108338418359a1facb939992fec9f24fcec79547e8a2bb76ddc969022a742556982d31d8171ef37ff43d0dba53f4e0020a16d4e5e060040420f00000000000000100000000200000000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000020970726573656e746572020101000000000000000000000000000000000000000000000000000000000000000000056167656e740001010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ecb5997cca9e941c7649e75bae5f8819248af8bff8b31bb56e8e69d47a6a3b11179394006d85f666de998d9175985b7ae7d3c613731c84d0696e4268bb90901", + "grants": [ + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c1d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af0000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000002010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c650000d2bb4d9f2bf3c4983bb769e2a76a6c402ad8067dfce4ea3222160fa161de3b6cfe058b530e006bc60f21de7004a61678d12b2bb9ba53609df31c489cd123e40e", + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c15599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf65670000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000100010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c6552000861695f757361676547007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a2261692d7573653d79227d7d38ce334dc24b7d6f2e7741f31af5656fdc808b9f86f8b2a260267443e844fde3fe13caaba3d5db439728c06d7563f2793f006b659aab4ab59ee3871f47ca5f0e", + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c184ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e90000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000100010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c65b5000861695f7573616765aa007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d792c2061692d7573653d79227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a307d7d59308bc2f1cc32bf9f2d6a2dd2147970988903a0e088ddfc7da06e6c9378099f3df134fa310f46010ecd3bff8e0b71fc32a9bc74082b373208574400c5a8d503" + ], + "records": [ + "5741553101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c10000000000000000000000000000000000000000000000000000000000000000000000000000000058007b2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a327d7d4c0edce29aa2c83e4fdce4cc1569c79f0d34e0ad8e5411855ac7fd4978dc3194580e417ce0c8796da55ea0a57fb3d531aedcfafa293a33ad68fc16cdd76aa50d" + ], + "outcome": { + "grants": "accept", + "records": { + "index": 0, + "reason": "usage_malformed" + }, + "terms": [ + { + "grant": "4b08089d169245776c7448605525fafbe3d5b810bd4fa0560c0b6c7873695d60", + "principal": "d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af", + "terms": null + }, + { + "grant": "5aaa435d2ebbfa1f014389c89737723314b85cc75276ef2dd233046e2be3bcc6", + "principal": "5599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf6567", + "terms": "8f22c1a79f254ccdfe1fdb9079024f9d6d9587c8ae0cfd1e34fcf092be1ee9a1" + }, + { + "grant": "91e8b7e1b875f610644a02af8e8fcfba174083546780f10240cd5bf3a0497e37", + "principal": "84ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e9", + "terms": "2df3d2b9315e76d2258ddcbfa338aa3bdf0657f399495027d8cc5bc40b3d0417" + } + ] + } + }, + { + "name": "record_truncated", + "description": "a record one byte short", + "descriptor": "57524431014e3d5118d9b3a594d897ae1b59d10473343108338418359a1facb939992fec9f24fcec79547e8a2bb76ddc969022a742556982d31d8171ef37ff43d0dba53f4e0020a16d4e5e060040420f00000000000000100000000200000000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000020970726573656e746572020101000000000000000000000000000000000000000000000000000000000000000000056167656e740001010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ecb5997cca9e941c7649e75bae5f8819248af8bff8b31bb56e8e69d47a6a3b11179394006d85f666de998d9175985b7ae7d3c613731c84d0696e4268bb90901", + "grants": [ + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c1d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af0000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000002010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c650000d2bb4d9f2bf3c4983bb769e2a76a6c402ad8067dfce4ea3222160fa161de3b6cfe058b530e006bc60f21de7004a61678d12b2bb9ba53609df31c489cd123e40e", + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c15599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf65670000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000100010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c6552000861695f757361676547007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a2261692d7573653d79227d7d38ce334dc24b7d6f2e7741f31af5656fdc808b9f86f8b2a260267443e844fde3fe13caaba3d5db439728c06d7563f2793f006b659aab4ab59ee3871f47ca5f0e", + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c184ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e90000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000100010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c65b5000861695f7573616765aa007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d792c2061692d7573653d79227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a307d7d59308bc2f1cc32bf9f2d6a2dd2147970988903a0e088ddfc7da06e6c9378099f3df134fa310f46010ecd3bff8e0b71fc32a9bc74082b373208574400c5a8d503" + ], + "records": [ + "5741553101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c100000000000000000000000000000000000000000000000000000000000000000000000000000000b4007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d6e2c2061692d7573653d792c207365617263683d6e227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a317d7d690f7ce597254323f05ee0cc8ae599dcfa4d895fb95d7273ab66459a17e5db8419366eb9e3a5688d9ab074f8b3ef0c43387e3a161e29d56311b93d233a5e1e" + ], + "outcome": { + "grants": "accept", + "records": { + "index": 0, + "reason": "truncated" + }, + "terms": [ + { + "grant": "4b08089d169245776c7448605525fafbe3d5b810bd4fa0560c0b6c7873695d60", + "principal": "d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af", + "terms": null + }, + { + "grant": "5aaa435d2ebbfa1f014389c89737723314b85cc75276ef2dd233046e2be3bcc6", + "principal": "5599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf6567", + "terms": "8f22c1a79f254ccdfe1fdb9079024f9d6d9587c8ae0cfd1e34fcf092be1ee9a1" + }, + { + "grant": "91e8b7e1b875f610644a02af8e8fcfba174083546780f10240cd5bf3a0497e37", + "principal": "84ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e9", + "terms": "2df3d2b9315e76d2258ddcbfa338aa3bdf0657f399495027d8cc5bc40b3d0417" + } + ] + } + }, + { + "name": "record_unknown_version", + "description": "a record of version 2", + "descriptor": "57524431014e3d5118d9b3a594d897ae1b59d10473343108338418359a1facb939992fec9f24fcec79547e8a2bb76ddc969022a742556982d31d8171ef37ff43d0dba53f4e0020a16d4e5e060040420f00000000000000100000000200000000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000020970726573656e746572020101000000000000000000000000000000000000000000000000000000000000000000056167656e740001010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ecb5997cca9e941c7649e75bae5f8819248af8bff8b31bb56e8e69d47a6a3b11179394006d85f666de998d9175985b7ae7d3c613731c84d0696e4268bb90901", + "grants": [ + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c1d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af0000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000002010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c650000d2bb4d9f2bf3c4983bb769e2a76a6c402ad8067dfce4ea3222160fa161de3b6cfe058b530e006bc60f21de7004a61678d12b2bb9ba53609df31c489cd123e40e", + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c15599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf65670000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000100010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c6552000861695f757361676547007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a2261692d7573653d79227d7d38ce334dc24b7d6f2e7741f31af5656fdc808b9f86f8b2a260267443e844fde3fe13caaba3d5db439728c06d7563f2793f006b659aab4ab59ee3871f47ca5f0e", + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c184ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e90000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000100010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c65b5000861695f7573616765aa007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d792c2061692d7573653d79227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a307d7d59308bc2f1cc32bf9f2d6a2dd2147970988903a0e088ddfc7da06e6c9378099f3df134fa310f46010ecd3bff8e0b71fc32a9bc74082b373208574400c5a8d503" + ], + "records": [ + "5741553102c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c100000000000000000000000000000000000000000000000000000000000000000000000000000000b4007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d6e2c2061692d7573653d792c207365617263683d6e227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a317d7d690f7ce597254323f05ee0cc8ae599dcfa4d895fb95d7273ab66459a17e5db8419366eb9e3a5688d9ab074f8b3ef0c43387e3a161e29d56311b93d233a5e1e0d" + ], + "outcome": { + "grants": "accept", + "records": { + "index": 0, + "reason": "unknown_version" + }, + "terms": [ + { + "grant": "4b08089d169245776c7448605525fafbe3d5b810bd4fa0560c0b6c7873695d60", + "principal": "d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af", + "terms": null + }, + { + "grant": "5aaa435d2ebbfa1f014389c89737723314b85cc75276ef2dd233046e2be3bcc6", + "principal": "5599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf6567", + "terms": "8f22c1a79f254ccdfe1fdb9079024f9d6d9587c8ae0cfd1e34fcf092be1ee9a1" + }, + { + "grant": "91e8b7e1b875f610644a02af8e8fcfba174083546780f10240cd5bf3a0497e37", + "principal": "84ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e9", + "terms": "2df3d2b9315e76d2258ddcbfa338aa3bdf0657f399495027d8cc5bc40b3d0417" + } + ] + } + }, + { + "name": "record_wrong_magic", + "description": "a record whose magic is not WAU1", + "descriptor": "57524431014e3d5118d9b3a594d897ae1b59d10473343108338418359a1facb939992fec9f24fcec79547e8a2bb76ddc969022a742556982d31d8171ef37ff43d0dba53f4e0020a16d4e5e060040420f00000000000000100000000200000000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000020970726573656e746572020101000000000000000000000000000000000000000000000000000000000000000000056167656e740001010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ecb5997cca9e941c7649e75bae5f8819248af8bff8b31bb56e8e69d47a6a3b11179394006d85f666de998d9175985b7ae7d3c613731c84d0696e4268bb90901", + "grants": [ + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c1d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af0000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000002010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c650000d2bb4d9f2bf3c4983bb769e2a76a6c402ad8067dfce4ea3222160fa161de3b6cfe058b530e006bc60f21de7004a61678d12b2bb9ba53609df31c489cd123e40e", + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c15599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf65670000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000100010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c6552000861695f757361676547007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a2261692d7573653d79227d7d38ce334dc24b7d6f2e7741f31af5656fdc808b9f86f8b2a260267443e844fde3fe13caaba3d5db439728c06d7563f2793f006b659aab4ab59ee3871f47ca5f0e", + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c184ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e90000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000100010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c65b5000861695f7573616765aa007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d792c2061692d7573653d79227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a307d7d59308bc2f1cc32bf9f2d6a2dd2147970988903a0e088ddfc7da06e6c9378099f3df134fa310f46010ecd3bff8e0b71fc32a9bc74082b373208574400c5a8d503" + ], + "records": [ + "5741553201c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c100000000000000000000000000000000000000000000000000000000000000000000000000000000b4007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d6e2c2061692d7573653d792c207365617263683d6e227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a317d7d690f7ce597254323f05ee0cc8ae599dcfa4d895fb95d7273ab66459a17e5db8419366eb9e3a5688d9ab074f8b3ef0c43387e3a161e29d56311b93d233a5e1e0d" + ], + "outcome": { + "grants": "accept", + "records": { + "index": 0, + "reason": "malformed" + }, + "terms": [ + { + "grant": "4b08089d169245776c7448605525fafbe3d5b810bd4fa0560c0b6c7873695d60", + "principal": "d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af", + "terms": null + }, + { + "grant": "5aaa435d2ebbfa1f014389c89737723314b85cc75276ef2dd233046e2be3bcc6", + "principal": "5599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf6567", + "terms": "8f22c1a79f254ccdfe1fdb9079024f9d6d9587c8ae0cfd1e34fcf092be1ee9a1" + }, + { + "grant": "91e8b7e1b875f610644a02af8e8fcfba174083546780f10240cd5bf3a0497e37", + "principal": "84ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e9", + "terms": "2df3d2b9315e76d2258ddcbfa338aa3bdf0657f399495027d8cc5bc40b3d0417" + } + ] + } + }, + { + "name": "grant_terms_malformed", + "description": "a grant whose terms do not read: the grant is held, with no terms", + "descriptor": "57524431014e3d5118d9b3a594d897ae1b59d10473343108338418359a1facb939992fec9f24fcec79547e8a2bb76ddc969022a742556982d31d8171ef37ff43d0dba53f4e0020a16d4e5e060040420f00000000000000100000000200000000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000020970726573656e746572020101000000000000000000000000000000000000000000000000000000000000000000056167656e740001010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ecb5997cca9e941c7649e75bae5f8819248af8bff8b31bb56e8e69d47a6a3b11179394006d85f666de998d9175985b7ae7d3c613731c84d0696e4268bb90901", + "grants": [ + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c1d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af0000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000002010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c650000d2bb4d9f2bf3c4983bb769e2a76a6c402ad8067dfce4ea3222160fa161de3b6cfe058b530e006bc60f21de7004a61678d12b2bb9ba53609df31c489cd123e40e", + "5752473101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c15599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf65670000000000000000000000000000000000000000000000000000000000000000000000000000000070f2d76a000000000052d96a000000000100010100000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011b68747470733a2f2f72656c61792e726f6f6d732e6578616d706c6521000861695f757361676516007b22616970726566223a202261692d7573653d79227d41b2a2df10652134470ac373f0f26c5557b592284fc20df6d1bcd71dc7c24c451feb4a415a40360903a1d313be24bd8fdbb42b7d42c268db6a9cab1c9e1c8905" + ], + "records": [ + "5741553101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c100000000000000000000000000000000000000000000000000000000000000000000000000000000b4007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d6e2c2061692d7573653d792c207365617263683d6e227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a317d7d690f7ce597254323f05ee0cc8ae599dcfa4d895fb95d7273ab66459a17e5db8419366eb9e3a5688d9ab074f8b3ef0c43387e3a161e29d56311b93d233a5e1e0d", + "5741553101c25682931e285e50d5a33e9a9ece16913feb1fc75ace031df6a9f3265a7759c1cd6140e57906f518cab1e2ffffad0b008c199658337d1f016d8d2893d19d08a60087930300000000b4007b22616970726566223a7b227265766973696f6e223a2264726166742d696574662d6169707265662d766f6361622d3038222c2276616c7565223a22747261696e2d61693d6e2c2061692d7573653d6e2c207365617263683d6e227d2c2274646d726570223a7b227265766973696f6e223a2243472d46494e414c2d74646d7265702d3230323430353130222c2274646d2d706f6c696379223a6e756c6c2c2274646d2d7265736572766174696f6e223a317d7db6e906d858eb9a5c45c5770ad433731938d702ce1465dfb43bb62b66d90d6a8015da4ed463781c80db5a60e9ef0db84269d66184e63e47a8b19dab13ebf75207" + ], + "outcome": { + "grants": "accept", + "records": "accept", + "terms": [ + { + "grant": "4b08089d169245776c7448605525fafbe3d5b810bd4fa0560c0b6c7873695d60", + "principal": "d61f0dac59144fb611e1af0801a972fba9fb471e7a05bcb0d02c226e3865e9af", + "terms": null + }, + { + "grant": "358d4803c1686550b4fd023beec941fc8c5dfda78c2c94a91a65af2d27a023b6", + "principal": "5599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf6567", + "terms": "grant_usage_malformed" + } + ], + "in_effect": [ + { + "t": 0, + "statements": [ + "033e03c9f353abb164f64ed3c147a8b4c2a7460f51376545e398e4c652f07908" + ] + }, + { + "t": 59000000, + "statements": [ + "033e03c9f353abb164f64ed3c147a8b4c2a7460f51376545e398e4c652f07908" + ] + }, + { + "t": 60000000, + "statements": [ + "3a7d7c0d1a6fdd2c35080206d58914c989a9de0ecd75d78b70b836361ad10960" + ] + }, + { + "t": 3600000000, + "statements": [ + "3a7d7c0d1a6fdd2c35080206d58914c989a9de0ecd75d78b70b836361ad10960" + ] + }, + { + "t": null, + "statements": [ + "033e03c9f353abb164f64ed3c147a8b4c2a7460f51376545e398e4c652f07908", + "3a7d7c0d1a6fdd2c35080206d58914c989a9de0ecd75d78b70b836361ad10960" + ] + } + ] + } + } + ], + "responses": [ + { + "name": "respelled", + "description": "the fields in another spelling: other order, no space, a parameter, an unknown key, other case", + "publication": "catalogue", + "object": "feature", + "fields": [ + [ + "content-usage", + "search=y,ai-use=y;x=1, train-ai=n, foo=y" + ], + [ + "TDM-Reservation", + " 1" + ], + [ + "tdm-policy", + "https://licensing.example/tdm-policy.json" + ] + ], + "outcome": { + "outcome": "accept", + "unsigned": [] + } + }, + { + "name": "split_lines", + "description": "Content-Usage on two field lines, combined", + "publication": "catalogue", + "object": "feature", + "fields": [ + [ + "Content-Usage", + "train-ai=n" + ], + [ + "Content-Usage", + "ai-use=y, search=y" + ] + ], + "outcome": { + "outcome": "accept", + "unsigned": [] + } + }, + { + "name": "aipref_contradicts", + "description": "Content-Usage allowing training the signed statement disallows", + "publication": "catalogue", + "object": "feature", + "fields": [ + [ + "Content-Usage", + "train-ai=y, ai-use=y, search=y" + ] + ], + "outcome": { + "outcome": "refuse", + "reason": "header_conflicts" + } + }, + { + "name": "aipref_drops_a_label", + "description": "Content-Usage that leaves search unknown", + "publication": "catalogue", + "object": "feature", + "fields": [ + [ + "Content-Usage", + "train-ai=n, ai-use=y" + ] + ], + "outcome": { + "outcome": "refuse", + "reason": "header_conflicts" + } + }, + { + "name": "aipref_unparseable", + "description": "Content-Usage that does not parse", + "publication": "catalogue", + "object": "feature", + "fields": [ + [ + "Content-Usage", + "train-ai=n,, ai-use=y, search=y" + ] + ], + "outcome": { + "outcome": "refuse", + "reason": "header_conflicts" + } + }, + { + "name": "tdm_contradicts", + "description": "tdm-reservation 0 against a signed 1", + "publication": "catalogue", + "object": "feature", + "fields": [ + [ + "tdm-reservation", + "0" + ] + ], + "outcome": { + "outcome": "refuse", + "reason": "header_conflicts" + } + }, + { + "name": "tdm_policy_contradicts", + "description": "a tdm-policy other than the signed one", + "publication": "catalogue", + "object": "feature", + "fields": [ + [ + "tdm-reservation", + "1" + ], + [ + "tdm-policy", + "https://elsewhere.example/p.json" + ] + ], + "outcome": { + "outcome": "refuse", + "reason": "header_conflicts" + } + }, + { + "name": "tdm_twice", + "description": "tdm-reservation on two field lines", + "publication": "catalogue", + "object": "feature", + "fields": [ + [ + "tdm-reservation", + "1" + ], + [ + "tdm-reservation", + "1" + ] + ], + "outcome": { + "outcome": "refuse", + "reason": "header_conflicts" + } + }, + { + "name": "unsigned_only", + "description": "fields for an object with no signed statement: unsigned statements, no conflict", + "publication": "catalogue", + "object": "credits", + "fields": [ + [ + "Content-Usage", + "train-ai=n" + ], + [ + "tdm-reservation", + "1" + ] + ], + "outcome": { + "outcome": "accept", + "unsigned": [ + "aipref", + "tdmrep" + ] + } + }, + { + "name": "unsigned_vocabulary", + "description": "a TDMRep field for an object whose statement carries aipref alone", + "publication": "catalogue", + "object": "trailer", + "fields": [ + [ + "Content-Usage", + "train-ai=n" + ], + [ + "tdm-reservation", + "1" + ] + ], + "outcome": { + "outcome": "accept", + "unsigned": [ + "tdmrep" + ] + } + } + ], + "catalogs": [ + { + "name": "agrees", + "description": "the catalog states the objects' statement, in another spelling", + "publication": "catalogue", + "objects": [ + "feature", + "stills" + ], + "entry": "{\"name\":\"feature\",\"packaging\":\"wai\",\"wai_capabilities\":[\"wai.video.av1\"],\"wai_ai_usage\":{ \"tdmrep\": {\"tdm-policy\": \"https://licensing.example/tdm-policy.json\", \"tdm-reservation\": 1, \"revision\": \"CG-FINAL-tdmrep-20240510\"}, \"cawg\": {\"entries\": {\"cawg.data_mining\": {\"constraint_info\": \"licensed on request: https://licensing.example/tdm\", \"use\": \"constrained\"}, \"cawg.ai_inference\": {\"use\": \"allowed\"}, \"cawg.ai_generative_training\": {\"use\": \"notAllowed\"}}, \"revision\": \"1.1\"}, \"aipref\": {\"value\": \"train-ai=n, ai-use=y, search=y\", \"revision\": \"draft-ietf-aipref-vocab-08\"} }}", + "outcome": { + "outcome": "accept" + } + }, + { + "name": "states_nothing", + "description": "a catalog that states no preference", + "publication": "catalogue", + "objects": [ + "feature", + "stills" + ], + "entry": "{\"name\":\"feature\",\"packaging\":\"wai\",\"wai_capabilities\":[\"wai.video.av1\"]}", + "outcome": { + "outcome": "accept" + } + }, + { + "name": "objects_state_nothing", + "description": "a catalog stating a preference over objects that carry none", + "publication": "catalogue", + "objects": [ + "credits" + ], + "entry": "{\"name\":\"feature\",\"packaging\":\"wai\",\"wai_capabilities\":[\"wai.video.av1\"],\"wai_ai_usage\":{\"aipref\":{\"revision\":\"draft-ietf-aipref-vocab-08\",\"value\":\"train-ai=y\"}}}", + "outcome": { + "outcome": "accept" + } + }, + { + "name": "contradicts", + "description": "a catalog allowing training the objects' statement disallows", + "publication": "catalogue", + "objects": [ + "feature", + "stills" + ], + "entry": "{\"name\":\"feature\",\"packaging\":\"wai\",\"wai_capabilities\":[\"wai.video.av1\"],\"wai_ai_usage\":{\"aipref\":{\"revision\":\"draft-ietf-aipref-vocab-08\",\"value\":\"train-ai=y\"}}}", + "outcome": { + "outcome": "refuse", + "reason": "catalog_conflicts" + } + }, + { + "name": "does_not_read", + "description": "a catalog whose statement does not read", + "publication": "catalogue", + "objects": [ + "feature" + ], + "entry": "{\"name\":\"feature\",\"packaging\":\"wai\",\"wai_capabilities\":[\"wai.video.av1\"],\"wai_ai_usage\":{}}", + "outcome": { + "outcome": "refuse", + "reason": "catalog_conflicts" + } + } + ], + "claims": [ + { + "name": "inference_allowed", + "description": "the crawler used the feature and the stills for inference, which the statement allows", + "wire": "{\"body\":{\"grant\":null,\"objects\":[\"3e0ea2c000e3b91fdc3705c72054c16ef27f008b23a9520104422fc887a6e2a4\",\"cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887\"],\"seen\":[\"1a55929b259aaf0c73c94a4f1ab932815ecd3c4a283c14ac3969b31568d52603\"],\"usage\":[{\"term\":\"ai-use\",\"vocabulary\":\"aipref\"},{\"term\":\"cawg.ai_inference\",\"vocabulary\":\"cawg\"},{\"term\":null,\"vocabulary\":\"tdmrep\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"4a48bb664e720496e8e06ed3ee9ee2688adbbbb717c7047359a8c1f361af54855cb84b0d56ea38a0a533700000c74d8b6fd771116da2fa217aa12fc7f3502007\",\"signer_id\":\"did:key:z6MkvXS98H92X1Y2DMRmDsGrtMXhsNZvCp2vcnh8NtVApVxh\",\"signer_pubkey\":\"eeccba9faecd6f65af29ba7714605fe62d2195e6f031c7b8736f816d3eeeeb36\",\"subject\":\"3e0ea2c000e3b91fdc3705c72054c16ef27f008b23a9520104422fc887a6e2a4\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "catalogue" + ], + "room": null, + "previous": null + }, + "outcome": { + "outcome": "accept", + "objects": [ + { + "object": "3e0ea2c000e3b91fdc3705c72054c16ef27f008b23a9520104422fc887a6e2a4", + "outcome": "allowed" + }, + { + "object": "cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887", + "outcome": "allowed" + } + ] + } + }, + { + "name": "unknown_use", + "description": "the crawler's second claim: inference over the trailer, whose statement says nothing of inference", + "wire": "{\"body\":{\"grant\":null,\"objects\":[\"8208d92f31db13460a23915c20213038a8391714ceae054f0dd8c72ba9299350\"],\"seen\":[\"8135caccc8321524d0b2735fb8ba848b82826091303aa4fb46eba37088dd0159\"],\"usage\":[{\"term\":\"ai-use\",\"vocabulary\":\"aipref\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":\"ea95cf8e1bdb6ec4bc8e48dd2b469a3a90847161ad60607614973ea4e4e6cf6e\",\"seq\":1,\"sig\":\"a275453b872a2c022736039fc2b5b1f7c407a31a169c58eb996f6b9ef5906aa49aa9be0a5e76731ab12a5d0ca87b89b0f650171f65ee6e48c7f445439abf230d\",\"signer_id\":\"did:key:z6MkvXS98H92X1Y2DMRmDsGrtMXhsNZvCp2vcnh8NtVApVxh\",\"signer_pubkey\":\"eeccba9faecd6f65af29ba7714605fe62d2195e6f031c7b8736f816d3eeeeb36\",\"subject\":\"8208d92f31db13460a23915c20213038a8391714ceae054f0dd8c72ba9299350\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "catalogue" + ], + "room": null, + "previous": "inference_allowed" + }, + "outcome": { + "outcome": "accept", + "objects": [ + { + "object": "8208d92f31db13460a23915c20213038a8391714ceae054f0dd8c72ba9299350", + "outcome": "unknown" + } + ] + } + }, + { + "name": "training_forbidden", + "description": "the crawler's third claim: generative training on the feature, which the statement disallows in every vocabulary", + "wire": "{\"body\":{\"grant\":null,\"objects\":[\"cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887\"],\"seen\":[\"1a55929b259aaf0c73c94a4f1ab932815ecd3c4a283c14ac3969b31568d52603\"],\"usage\":[{\"term\":\"train-ai\",\"vocabulary\":\"aipref\"},{\"term\":\"cawg.ai_generative_training\",\"vocabulary\":\"cawg\"},{\"term\":\"tdm-reservation\",\"vocabulary\":\"tdmrep\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":\"799fd0e38701d3c69dd673d297e19bcee1cab5445dce406037ab85d809cc4e87\",\"seq\":2,\"sig\":\"bbf8628932f3d3472ab4f0a341afeae2de7be2505b836c3ca1b5d70c2d7f8835f4bf94c95cd47656996dac98a5398094da3dc7822b6d793e07ac61ce7aef0802\",\"signer_id\":\"did:key:z6MkvXS98H92X1Y2DMRmDsGrtMXhsNZvCp2vcnh8NtVApVxh\",\"signer_pubkey\":\"eeccba9faecd6f65af29ba7714605fe62d2195e6f031c7b8736f816d3eeeeb36\",\"subject\":\"cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "catalogue" + ], + "room": null, + "previous": "unknown_use" + }, + "outcome": { + "outcome": "refuse", + "step": 6, + "reason": "usage_disallowed" + } + }, + { + "name": "outside", + "description": "a use outside the vocabulary, over an object with no statement", + "wire": "{\"body\":{\"grant\":null,\"objects\":[\"cf8bac17e7673c6b99f3854f4cc376ed1e168e47eae8f0e9316269122586e9d0\"],\"seen\":[],\"usage\":[{\"term\":null,\"vocabulary\":\"aipref\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"099b0375bcfade27b120f6d2666fd679df8550afd67834180a29162993714d24c57bc9aaa02c44c0e8e3bbe604b02bd309216e9dec39ddf0a289d3dd4879d604\",\"signer_id\":\"did:key:z6Mkn6VVhrUZZXmct9hUxkuYjhfsimKbL7Z8wRSTBEBvYUZv\",\"signer_pubkey\":\"718b81b425ebfbd5c5da76f32d5550760bf023e24df858d2d9235a6e79cabfe9\",\"subject\":\"cf8bac17e7673c6b99f3854f4cc376ed1e168e47eae8f0e9316269122586e9d0\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "catalogue" + ], + "room": null, + "previous": null + }, + "outcome": { + "outcome": "accept", + "objects": [ + { + "object": "cf8bac17e7673c6b99f3854f4cc376ed1e168e47eae8f0e9316269122586e9d0", + "outcome": "outside" + } + ] + } + }, + { + "name": "mining_constrained", + "description": "mining the feature: constrained in the assertion, which reads as not allowed without a grant", + "wire": "{\"body\":{\"grant\":null,\"objects\":[\"cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887\"],\"seen\":[\"1a55929b259aaf0c73c94a4f1ab932815ecd3c4a283c14ac3969b31568d52603\"],\"usage\":[{\"term\":\"ai-use\",\"vocabulary\":\"aipref\"},{\"term\":\"cawg.data_mining\",\"vocabulary\":\"cawg\"},{\"term\":null,\"vocabulary\":\"tdmrep\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"c631af3a3649cf0e871642585189e504dc55b371bb823b85d3a67da2f71d24cb886442422f644ba8fb75e04b93ac8c165410b165c4dbce5f9345b0eecaea3e02\",\"signer_id\":\"did:key:z6Mkn6VVhrUZZXmct9hUxkuYjhfsimKbL7Z8wRSTBEBvYUZv\",\"signer_pubkey\":\"718b81b425ebfbd5c5da76f32d5550760bf023e24df858d2d9235a6e79cabfe9\",\"subject\":\"cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "catalogue" + ], + "room": null, + "previous": null + }, + "outcome": { + "outcome": "refuse", + "step": 6, + "reason": "usage_disallowed" + } + }, + { + "name": "wrong_digests", + "description": "a claim naming an object the verifier does not hold: the stripped feature's content hash", + "wire": "{\"body\":{\"grant\":null,\"objects\":[\"9461b0b5521f5dfc111273ad0041173c96130e63e84adf588fa1b28df99bf4cf\"],\"seen\":[],\"usage\":[{\"term\":\"ai-use\",\"vocabulary\":\"aipref\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"d15dbc4fb988c12bc15830d48f212831a22ff4bca86bcbc965ec4c4573af49f9e668e2b278d4309d35bcf036191958a5ccacdd5539ec8171d78a843fe765e705\",\"signer_id\":\"did:key:z6Mkn6VVhrUZZXmct9hUxkuYjhfsimKbL7Z8wRSTBEBvYUZv\",\"signer_pubkey\":\"718b81b425ebfbd5c5da76f32d5550760bf023e24df858d2d9235a6e79cabfe9\",\"subject\":\"9461b0b5521f5dfc111273ad0041173c96130e63e84adf588fa1b28df99bf4cf\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "catalogue" + ], + "room": null, + "previous": null + }, + "outcome": { + "outcome": "refuse", + "step": 6, + "reason": "consumption_object_unknown" + } + }, + { + "name": "bad_signature", + "description": "an accepted claim with one bit of its signature changed", + "wire": "{\"body\":{\"grant\":null,\"objects\":[\"3e0ea2c000e3b91fdc3705c72054c16ef27f008b23a9520104422fc887a6e2a4\",\"cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887\"],\"seen\":[\"1a55929b259aaf0c73c94a4f1ab932815ecd3c4a283c14ac3969b31568d52603\"],\"usage\":[{\"term\":\"ai-use\",\"vocabulary\":\"aipref\"},{\"term\":\"cawg.ai_inference\",\"vocabulary\":\"cawg\"},{\"term\":null,\"vocabulary\":\"tdmrep\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"2d13dd98ab64e4c692709ac8e0f0bb81c72b24ef4cfa6b336ae22083002ccd13c4f99b1785b0dd0421b463299bdc757b5f6d61925750f2d0657e550233c11a07\",\"signer_id\":\"did:key:z6Mkn6VVhrUZZXmct9hUxkuYjhfsimKbL7Z8wRSTBEBvYUZv\",\"signer_pubkey\":\"718b81b425ebfbd5c5da76f32d5550760bf023e24df858d2d9235a6e79cabfe9\",\"subject\":\"3e0ea2c000e3b91fdc3705c72054c16ef27f008b23a9520104422fc887a6e2a4\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "catalogue" + ], + "room": null, + "previous": null + }, + "outcome": { + "outcome": "refuse", + "step": 3, + "reason": "signature_verify_failed" + } + }, + { + "name": "preference_mismatch", + "description": "a claim that says it saw a statement other than the one that governs", + "wire": "{\"body\":{\"grant\":null,\"objects\":[\"cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887\"],\"seen\":[\"8135caccc8321524d0b2735fb8ba848b82826091303aa4fb46eba37088dd0159\"],\"usage\":[{\"term\":\"ai-use\",\"vocabulary\":\"aipref\"},{\"term\":\"cawg.ai_inference\",\"vocabulary\":\"cawg\"},{\"term\":null,\"vocabulary\":\"tdmrep\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"be8c9ba51df974f77729953762c260b8ca217c8767b0ceebb64b19d7d4ce54e32233207ba428ec2cd3ff33063847055f44f9eef9ccc93ea06277bec42ca2e007\",\"signer_id\":\"did:key:z6Mkn6VVhrUZZXmct9hUxkuYjhfsimKbL7Z8wRSTBEBvYUZv\",\"signer_pubkey\":\"718b81b425ebfbd5c5da76f32d5550760bf023e24df858d2d9235a6e79cabfe9\",\"subject\":\"cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "catalogue" + ], + "room": null, + "previous": null + }, + "outcome": { + "outcome": "refuse", + "step": 6, + "reason": "preference_mismatch" + } + }, + { + "name": "unanswered", + "description": "a claim answering aipref alone over an object whose statement carries all three vocabularies", + "wire": "{\"body\":{\"grant\":null,\"objects\":[\"cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887\"],\"seen\":[\"1a55929b259aaf0c73c94a4f1ab932815ecd3c4a283c14ac3969b31568d52603\"],\"usage\":[{\"term\":\"ai-use\",\"vocabulary\":\"aipref\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"3c3d2c8a9d61041e688379dddcd59460b2436f4b29857be5f43011bfba384cd5f7970032b15fb2692fd6c71d21a3f2d9cb20af6e64ff0eccc1833e64a08e6d02\",\"signer_id\":\"did:key:z6Mkn6VVhrUZZXmct9hUxkuYjhfsimKbL7Z8wRSTBEBvYUZv\",\"signer_pubkey\":\"718b81b425ebfbd5c5da76f32d5550760bf023e24df858d2d9235a6e79cabfe9\",\"subject\":\"cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "catalogue" + ], + "room": null, + "previous": null + }, + "outcome": { + "outcome": "refuse", + "step": 6, + "reason": "usage_unanswered" + } + }, + { + "name": "term_invented", + "description": "a term no vocabulary defines", + "wire": "{\"body\":{\"grant\":null,\"objects\":[\"8208d92f31db13460a23915c20213038a8391714ceae054f0dd8c72ba9299350\"],\"seen\":[\"8135caccc8321524d0b2735fb8ba848b82826091303aa4fb46eba37088dd0159\"],\"usage\":[{\"term\":\"inference\",\"vocabulary\":\"aipref\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"7eeab01e35160635ac06fdbcb6367fabb4321afdfe13bbd477660e90e59672f8517c51757c282d9851f0c1a5975996ce28425e2e73fbfee96a662c66b465a408\",\"signer_id\":\"did:key:z6Mkn6VVhrUZZXmct9hUxkuYjhfsimKbL7Z8wRSTBEBvYUZv\",\"signer_pubkey\":\"718b81b425ebfbd5c5da76f32d5550760bf023e24df858d2d9235a6e79cabfe9\",\"subject\":\"8208d92f31db13460a23915c20213038a8391714ceae054f0dd8c72ba9299350\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "catalogue" + ], + "room": null, + "previous": null + }, + "outcome": { + "outcome": "refuse", + "step": 4, + "reason": "consumption_term_unknown" + } + }, + { + "name": "cawg_term_reserved", + "description": "a cawg. key the assertion does not define", + "wire": "{\"body\":{\"grant\":null,\"objects\":[\"cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887\"],\"seen\":[\"1a55929b259aaf0c73c94a4f1ab932815ecd3c4a283c14ac3969b31568d52603\"],\"usage\":[{\"term\":\"ai-use\",\"vocabulary\":\"aipref\"},{\"term\":\"cawg.ai_summaries\",\"vocabulary\":\"cawg\"},{\"term\":null,\"vocabulary\":\"tdmrep\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"7ce79d50ac1de4fc5fb03b157e0c1418a4a9bec877c1f59a731bb942d8f3c1939f459138e76735ca175d232925c72b593dcbcb76b0551b28f7bbda4ffd45fe03\",\"signer_id\":\"did:key:z6Mkn6VVhrUZZXmct9hUxkuYjhfsimKbL7Z8wRSTBEBvYUZv\",\"signer_pubkey\":\"718b81b425ebfbd5c5da76f32d5550760bf023e24df858d2d9235a6e79cabfe9\",\"subject\":\"cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "catalogue" + ], + "room": null, + "previous": null + }, + "outcome": { + "outcome": "refuse", + "step": 4, + "reason": "consumption_term_unknown" + } + }, + { + "name": "objects_unordered", + "description": "objects in descending order", + "wire": "{\"body\":{\"grant\":null,\"objects\":[\"cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887\",\"3e0ea2c000e3b91fdc3705c72054c16ef27f008b23a9520104422fc887a6e2a4\"],\"seen\":[\"1a55929b259aaf0c73c94a4f1ab932815ecd3c4a283c14ac3969b31568d52603\"],\"usage\":[{\"term\":\"ai-use\",\"vocabulary\":\"aipref\"},{\"term\":\"cawg.ai_inference\",\"vocabulary\":\"cawg\"},{\"term\":null,\"vocabulary\":\"tdmrep\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"eca391e4f98b189c549176d8302631d5c49a7a8ef489653b75a4a956c3cb17e39afd336bac0c730064932d812b3a514e6bfe0bdf40725f05021d8db89cf1b906\",\"signer_id\":\"did:key:z6Mkn6VVhrUZZXmct9hUxkuYjhfsimKbL7Z8wRSTBEBvYUZv\",\"signer_pubkey\":\"718b81b425ebfbd5c5da76f32d5550760bf023e24df858d2d9235a6e79cabfe9\",\"subject\":\"cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "catalogue" + ], + "room": null, + "previous": null + }, + "outcome": { + "outcome": "refuse", + "step": 4, + "reason": "consumption_objects_unordered" + } + }, + { + "name": "subject_not_first", + "description": "a subject other than the first object", + "wire": "{\"body\":{\"grant\":null,\"objects\":[\"3e0ea2c000e3b91fdc3705c72054c16ef27f008b23a9520104422fc887a6e2a4\",\"cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887\"],\"seen\":[\"1a55929b259aaf0c73c94a4f1ab932815ecd3c4a283c14ac3969b31568d52603\"],\"usage\":[{\"term\":\"ai-use\",\"vocabulary\":\"aipref\"},{\"term\":\"cawg.ai_inference\",\"vocabulary\":\"cawg\"},{\"term\":null,\"vocabulary\":\"tdmrep\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"ee1175955040975f5b80c308b4a642aa052e8636f09ac49e92785ce0c70c2dfcb4b5c16c5383902ce8d099df83999291775d223df78e57d8a43322cbfeedf403\",\"signer_id\":\"did:key:z6Mkn6VVhrUZZXmct9hUxkuYjhfsimKbL7Z8wRSTBEBvYUZv\",\"signer_pubkey\":\"718b81b425ebfbd5c5da76f32d5550760bf023e24df858d2d9235a6e79cabfe9\",\"subject\":\"cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "catalogue" + ], + "room": null, + "previous": null + }, + "outcome": { + "outcome": "refuse", + "step": 4, + "reason": "consumption_subject_mismatch" + } + }, + { + "name": "usage_unordered", + "description": "usage entries out of vocabulary order", + "wire": "{\"body\":{\"grant\":null,\"objects\":[\"cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887\"],\"seen\":[\"1a55929b259aaf0c73c94a4f1ab932815ecd3c4a283c14ac3969b31568d52603\"],\"usage\":[{\"term\":null,\"vocabulary\":\"tdmrep\"},{\"term\":\"ai-use\",\"vocabulary\":\"aipref\"},{\"term\":\"cawg.ai_inference\",\"vocabulary\":\"cawg\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"54dc55a0403b3d483d01fd919e8601e8327a1787775111bae5fed6821b0b5d0de9fcf0d8b851560afea5038cff972f8756f8c7171df39af1f68a4efad9929f0a\",\"signer_id\":\"did:key:z6Mkn6VVhrUZZXmct9hUxkuYjhfsimKbL7Z8wRSTBEBvYUZv\",\"signer_pubkey\":\"718b81b425ebfbd5c5da76f32d5550760bf023e24df858d2d9235a6e79cabfe9\",\"subject\":\"cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "catalogue" + ], + "room": null, + "previous": null + }, + "outcome": { + "outcome": "refuse", + "step": 4, + "reason": "consumption_usage_unordered" + } + }, + { + "name": "seen_unordered", + "description": "seen digests in descending order", + "wire": "{\"body\":{\"grant\":null,\"objects\":[\"8208d92f31db13460a23915c20213038a8391714ceae054f0dd8c72ba9299350\",\"cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887\"],\"seen\":[\"8135caccc8321524d0b2735fb8ba848b82826091303aa4fb46eba37088dd0159\",\"1a55929b259aaf0c73c94a4f1ab932815ecd3c4a283c14ac3969b31568d52603\"],\"usage\":[{\"term\":\"ai-use\",\"vocabulary\":\"aipref\"},{\"term\":\"cawg.ai_inference\",\"vocabulary\":\"cawg\"},{\"term\":null,\"vocabulary\":\"tdmrep\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"d138c969e304a73790566f2c5fae191887ba7bce0568462879290f0f85925ae6755e67c2483c71d69ecb8189e1ea7647cdfef7704693c814e0457d6fec28cb0e\",\"signer_id\":\"did:key:z6Mkn6VVhrUZZXmct9hUxkuYjhfsimKbL7Z8wRSTBEBvYUZv\",\"signer_pubkey\":\"718b81b425ebfbd5c5da76f32d5550760bf023e24df858d2d9235a6e79cabfe9\",\"subject\":\"8208d92f31db13460a23915c20213038a8391714ceae054f0dd8c72ba9299350\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "catalogue" + ], + "room": null, + "previous": null + }, + "outcome": { + "outcome": "refuse", + "step": 4, + "reason": "consumption_seen_unordered" + } + }, + { + "name": "signer_not_principal", + "description": "a signer_id that is not the did:key of the signing key", + "wire": "{\"body\":{\"grant\":null,\"objects\":[\"cf8bac17e7673c6b99f3854f4cc376ed1e168e47eae8f0e9316269122586e9d0\"],\"seen\":[],\"usage\":[{\"term\":null,\"vocabulary\":\"aipref\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"03043dcaf97bc27f81ed1b333e9366531490d4cd82da59e3b13f83453b272ac2df5c8445482e39fdf2f4411da47368d3e819b921a88d67c3c063cfe12b02af05\",\"signer_id\":\"did:key:z6MktotDXKTuzYL2CFw1MqK5PeFeBvawxvgwFB31VnQZq5oq\",\"signer_pubkey\":\"718b81b425ebfbd5c5da76f32d5550760bf023e24df858d2d9235a6e79cabfe9\",\"subject\":\"cf8bac17e7673c6b99f3854f4cc376ed1e168e47eae8f0e9316269122586e9d0\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "catalogue" + ], + "room": null, + "previous": null + }, + "outcome": { + "outcome": "refuse", + "step": 4, + "reason": "consumption_signer_not_principal" + } + }, + { + "name": "vocabulary_unknown", + "description": "a usage entry naming a vocabulary outside the three", + "wire": "{\"body\":{\"grant\":null,\"objects\":[\"cf8bac17e7673c6b99f3854f4cc376ed1e168e47eae8f0e9316269122586e9d0\"],\"seen\":[],\"usage\":[{\"term\":null,\"vocabulary\":\"robots\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"099b0375bcfade27b120f6d2666fd679df8550afd67834180a29162993714d24c57bc9aaa02c44c0e8e3bbe604b02bd309216e9dec39ddf0a289d3dd4879d604\",\"signer_id\":\"did:key:z6Mkn6VVhrUZZXmct9hUxkuYjhfsimKbL7Z8wRSTBEBvYUZv\",\"signer_pubkey\":\"718b81b425ebfbd5c5da76f32d5550760bf023e24df858d2d9235a6e79cabfe9\",\"subject\":\"cf8bac17e7673c6b99f3854f4cc376ed1e168e47eae8f0e9316269122586e9d0\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "catalogue" + ], + "room": null, + "previous": null + }, + "outcome": { + "outcome": "refuse", + "step": 2, + "reason": "field_type" + } + }, + { + "name": "parent_not_given", + "description": "a second claim whose first the verifier is not given", + "wire": "{\"body\":{\"grant\":null,\"objects\":[\"cf8bac17e7673c6b99f3854f4cc376ed1e168e47eae8f0e9316269122586e9d0\"],\"seen\":[],\"usage\":[{\"term\":null,\"vocabulary\":\"aipref\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":\"b51de5544c6844685b2c80308c01ea2c82cd40131ea80629fc0e25427a89a833\",\"seq\":1,\"sig\":\"b07b2e69474ab05ff69499389426800a6547041ce968c37363a34ea6f6789e5c86700e635883a009c9e797c8cb69aecc101b30f95a7897fc930ee146a4dfa002\",\"signer_id\":\"did:key:z6Mkn6VVhrUZZXmct9hUxkuYjhfsimKbL7Z8wRSTBEBvYUZv\",\"signer_pubkey\":\"718b81b425ebfbd5c5da76f32d5550760bf023e24df858d2d9235a6e79cabfe9\",\"subject\":\"cf8bac17e7673c6b99f3854f4cc376ed1e168e47eae8f0e9316269122586e9d0\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "catalogue" + ], + "room": null, + "previous": null + }, + "outcome": { + "outcome": "refuse", + "step": 5, + "reason": "parent_not_held" + } + }, + { + "name": "room_inference", + "description": "agent A, under its grant, used the lecture at 10 s for inference, which the room's first record allows", + "wire": "{\"body\":{\"grant\":\"5aaa435d2ebbfa1f014389c89737723314b85cc75276ef2dd233046e2be3bcc6\",\"objects\":[\"e09627f6f863adc91750c6193680047d1688f5f718aaed7e745334fe1c1712b0\"],\"seen\":[\"033e03c9f353abb164f64ed3c147a8b4c2a7460f51376545e398e4c652f07908\"],\"usage\":[{\"term\":\"ai-use\",\"vocabulary\":\"aipref\"},{\"term\":null,\"vocabulary\":\"tdmrep\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"77f1cf994167fdb138f81481127d498a7a1abc8f595e8e1f783ad70c308d82f31d7ed1f2bbfe009efb60976f51c356eb87ee01f382a50665a17598675faab60f\",\"signer_id\":\"did:key:z6MkkDQnUB6MNmYRGUhq13Pewf7EoDCJmtUULdMHhPu8uhn6\",\"signer_pubkey\":\"5599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf6567\",\"subject\":\"e09627f6f863adc91750c6193680047d1688f5f718aaed7e745334fe1c1712b0\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "lecture_media" + ], + "room": "lecture", + "previous": null + }, + "outcome": { + "outcome": "accept", + "objects": [ + { + "object": "e09627f6f863adc91750c6193680047d1688f5f718aaed7e745334fe1c1712b0", + "outcome": "allowed" + } + ] + } + }, + { + "name": "room_terms_lift", + "description": "agent A used the lecture at 70 s for inference: the second record disallows it, and A's terms allow it", + "wire": "{\"body\":{\"grant\":\"5aaa435d2ebbfa1f014389c89737723314b85cc75276ef2dd233046e2be3bcc6\",\"objects\":[\"6ee066b579606409d041380760beff71e7f1589d838c270afd0781c1db377fd6\"],\"seen\":[\"3a7d7c0d1a6fdd2c35080206d58914c989a9de0ecd75d78b70b836361ad10960\"],\"usage\":[{\"term\":\"ai-use\",\"vocabulary\":\"aipref\"},{\"term\":null,\"vocabulary\":\"tdmrep\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":\"218b502e38898881ab2234988720a3ec3e37a6429ec6e2407570e1ad9c7660d7\",\"seq\":1,\"sig\":\"a278a32ba1d7b621f635597130f6551ec8655f0154e684619cb34eeec5d48d3a497731f74cf2f21309ea93aa2187a62f7277b89ba5991f700e39711fdb9a700e\",\"signer_id\":\"did:key:z6MkkDQnUB6MNmYRGUhq13Pewf7EoDCJmtUULdMHhPu8uhn6\",\"signer_pubkey\":\"5599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf6567\",\"subject\":\"6ee066b579606409d041380760beff71e7f1589d838c270afd0781c1db377fd6\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "lecture_media" + ], + "room": "lecture", + "previous": "room_inference" + }, + "outcome": { + "outcome": "accept", + "objects": [ + { + "object": "6ee066b579606409d041380760beff71e7f1589d838c270afd0781c1db377fd6", + "outcome": "allowed" + } + ] + } + }, + { + "name": "room_training_unlicensed", + "description": "agent A trained on the lecture at 10 s: its terms say nothing of training, and the room disallows it", + "wire": "{\"body\":{\"grant\":\"5aaa435d2ebbfa1f014389c89737723314b85cc75276ef2dd233046e2be3bcc6\",\"objects\":[\"e09627f6f863adc91750c6193680047d1688f5f718aaed7e745334fe1c1712b0\"],\"seen\":[\"033e03c9f353abb164f64ed3c147a8b4c2a7460f51376545e398e4c652f07908\"],\"usage\":[{\"term\":\"train-ai\",\"vocabulary\":\"aipref\"},{\"term\":\"tdm-reservation\",\"vocabulary\":\"tdmrep\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":\"c6f508b52b36ff760862299bfc717344c7a7bdfeb48105550f01796036113366\",\"seq\":2,\"sig\":\"0b907dd76b164ef48441e0abe758ab52403163a88273961c8f01862720c4165263f9b97fb01f785e0ffc8819a98dca1ec928bfc3807dc02b9727d274245a3809\",\"signer_id\":\"did:key:z6MkkDQnUB6MNmYRGUhq13Pewf7EoDCJmtUULdMHhPu8uhn6\",\"signer_pubkey\":\"5599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf6567\",\"subject\":\"e09627f6f863adc91750c6193680047d1688f5f718aaed7e745334fe1c1712b0\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "lecture_media" + ], + "room": "lecture", + "previous": "room_terms_lift" + }, + "outcome": { + "outcome": "refuse", + "step": 6, + "reason": "usage_disallowed" + } + }, + { + "name": "room_untimed", + "description": "an object of the room with no live.t is governed by every record; A's terms allow inference", + "wire": "{\"body\":{\"grant\":\"5aaa435d2ebbfa1f014389c89737723314b85cc75276ef2dd233046e2be3bcc6\",\"objects\":[\"18b622c8e2b90b10d6b9af355467881354d1cf3a6a4e2b10f8ea6be3e0946f2b\"],\"seen\":[\"033e03c9f353abb164f64ed3c147a8b4c2a7460f51376545e398e4c652f07908\",\"3a7d7c0d1a6fdd2c35080206d58914c989a9de0ecd75d78b70b836361ad10960\"],\"usage\":[{\"term\":\"ai-use\",\"vocabulary\":\"aipref\"},{\"term\":null,\"vocabulary\":\"tdmrep\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":\"558dd9801ff37ac999dadca8ebe176d321acace9c15e4edda46c87f6eb5a23f0\",\"seq\":3,\"sig\":\"941aab6298f5c39181166acfb236c22ae396cdebb82759e34b30ee84922d87a211860285c60e2b1b2ec81e45ddcdfe273b12222898a4375ab21d960e51ab2d0e\",\"signer_id\":\"did:key:z6MkkDQnUB6MNmYRGUhq13Pewf7EoDCJmtUULdMHhPu8uhn6\",\"signer_pubkey\":\"5599df2be02f4fd63b6f6759301bcee2b4ee5beb125f4e4519dfe78937cf6567\",\"subject\":\"18b622c8e2b90b10d6b9af355467881354d1cf3a6a4e2b10f8ea6be3e0946f2b\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "lecture_media" + ], + "room": "lecture", + "previous": "room_training_unlicensed" + }, + "outcome": { + "outcome": "accept", + "objects": [ + { + "object": "18b622c8e2b90b10d6b9af355467881354d1cf3a6a4e2b10f8ea6be3e0946f2b", + "outcome": "allowed" + } + ] + } + }, + { + "name": "room_licensed_training", + "description": "agent B trained on the lecture at 10 s under a licence that lifts the room's disallow in both vocabularies", + "wire": "{\"body\":{\"grant\":\"91e8b7e1b875f610644a02af8e8fcfba174083546780f10240cd5bf3a0497e37\",\"objects\":[\"e09627f6f863adc91750c6193680047d1688f5f718aaed7e745334fe1c1712b0\"],\"seen\":[\"033e03c9f353abb164f64ed3c147a8b4c2a7460f51376545e398e4c652f07908\"],\"usage\":[{\"term\":\"train-ai\",\"vocabulary\":\"aipref\"},{\"term\":\"tdm-reservation\",\"vocabulary\":\"tdmrep\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"82419b04f2d8b8008ca3f9a45303fdfec114e93fc3f78bc86de1e5884529e47c6401aa2221485513242356afacdcc06f4410925a6024590f9b706c6cc4ce4002\",\"signer_id\":\"did:key:z6MkoQAbM6zxdqVs5zcKehr5vofaYgpZWqLui1Q3pVoA5PQk\",\"signer_pubkey\":\"84ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e9\",\"subject\":\"e09627f6f863adc91750c6193680047d1688f5f718aaed7e745334fe1c1712b0\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "lecture_media" + ], + "room": "lecture", + "previous": null + }, + "outcome": { + "outcome": "accept", + "objects": [ + { + "object": "e09627f6f863adc91750c6193680047d1688f5f718aaed7e745334fe1c1712b0", + "outcome": "allowed" + } + ] + } + }, + { + "name": "room_own_and_room", + "description": "search over an object whose own statement allows it and whose room disallows it: the most restrictive governs", + "wire": "{\"body\":{\"grant\":\"91e8b7e1b875f610644a02af8e8fcfba174083546780f10240cd5bf3a0497e37\",\"objects\":[\"492bce59489cd76693ced443338c46a5ae22b0570af1887db34aa70e8ba28a95\"],\"seen\":[\"033e03c9f353abb164f64ed3c147a8b4c2a7460f51376545e398e4c652f07908\",\"d186796906e025546774b20e0aaa936e929d7e1702faefe764d03e34ffe0eef5\"],\"usage\":[{\"term\":\"search\",\"vocabulary\":\"aipref\"},{\"term\":null,\"vocabulary\":\"tdmrep\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":\"20416b3f3ba360a68da5f1b959517b91680be9bcc873f00fb011083dbd875990\",\"seq\":1,\"sig\":\"7337f4b5c02c12aaae3ed35bb3eb96b22765a90a94eac2841b954bca1c9c2a752f8996f094ee798ecb2d0fba1c8ea176e0c7ba2913d2416e53d271de7c223b03\",\"signer_id\":\"did:key:z6MkoQAbM6zxdqVs5zcKehr5vofaYgpZWqLui1Q3pVoA5PQk\",\"signer_pubkey\":\"84ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e9\",\"subject\":\"492bce59489cd76693ced443338c46a5ae22b0570af1887db34aa70e8ba28a95\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "lecture_media" + ], + "room": "lecture", + "previous": "room_licensed_training" + }, + "outcome": { + "outcome": "refuse", + "step": 6, + "reason": "usage_disallowed" + } + }, + { + "name": "room_grant_on_another_object", + "description": "agent B naming its grant over an object that is not the room's", + "wire": "{\"body\":{\"grant\":\"91e8b7e1b875f610644a02af8e8fcfba174083546780f10240cd5bf3a0497e37\",\"objects\":[\"cf8bac17e7673c6b99f3854f4cc376ed1e168e47eae8f0e9316269122586e9d0\"],\"seen\":[],\"usage\":[{\"term\":null,\"vocabulary\":\"aipref\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":\"c767f6e39402b7f851b7718f773e88cb6c65e1f339fae6cadfc44450593e0223\",\"seq\":2,\"sig\":\"1b46a26492047908d64c90bbffcca53b2a6fec35baae13b73fa2de5149dbd3ce1f96232b7157aca153eb4e2ba4e0e9606636d569cb829dac8dc1777199834809\",\"signer_id\":\"did:key:z6MkoQAbM6zxdqVs5zcKehr5vofaYgpZWqLui1Q3pVoA5PQk\",\"signer_pubkey\":\"84ee465769c334d0080e31c89fe499cce925ee4db213b9077f903e83294083e9\",\"subject\":\"cf8bac17e7673c6b99f3854f4cc376ed1e168e47eae8f0e9316269122586e9d0\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "catalogue", + "lecture_media" + ], + "room": "lecture", + "previous": "room_own_and_room" + }, + "outcome": { + "outcome": "refuse", + "step": 6, + "reason": "grant_mismatch" + } + }, + { + "name": "room_grant_not_the_signers", + "description": "a key that is not agent B's naming agent B's grant", + "wire": "{\"body\":{\"grant\":\"91e8b7e1b875f610644a02af8e8fcfba174083546780f10240cd5bf3a0497e37\",\"objects\":[\"e09627f6f863adc91750c6193680047d1688f5f718aaed7e745334fe1c1712b0\"],\"seen\":[\"033e03c9f353abb164f64ed3c147a8b4c2a7460f51376545e398e4c652f07908\"],\"usage\":[{\"term\":\"ai-use\",\"vocabulary\":\"aipref\"},{\"term\":null,\"vocabulary\":\"tdmrep\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"849ad9b0d7454546c9f7e2e5b6de5d18da81fcf543562ef0a103576c164eee0dc04d71603f818c471fac217569872cdc25e175f1991ac3b4411b8f63d3d6940b\",\"signer_id\":\"did:key:z6MktotDXKTuzYL2CFw1MqK5PeFeBvawxvgwFB31VnQZq5oq\",\"signer_pubkey\":\"d54c21d1e62389f2f128767a6cdbaa320fa27fd9ba1f9463c3d92cb210d5054c\",\"subject\":\"e09627f6f863adc91750c6193680047d1688f5f718aaed7e745334fe1c1712b0\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "lecture_media" + ], + "room": "lecture", + "previous": null + }, + "outcome": { + "outcome": "refuse", + "step": 6, + "reason": "grant_mismatch" + } + }, + { + "name": "room_without_grant", + "description": "an agent with no grant using the room's object for inference: the room's first record governs", + "wire": "{\"body\":{\"grant\":null,\"objects\":[\"e09627f6f863adc91750c6193680047d1688f5f718aaed7e745334fe1c1712b0\"],\"seen\":[\"033e03c9f353abb164f64ed3c147a8b4c2a7460f51376545e398e4c652f07908\"],\"usage\":[{\"term\":\"ai-use\",\"vocabulary\":\"aipref\"},{\"term\":null,\"vocabulary\":\"tdmrep\"}]},\"class\":\"consumption\",\"issued_at\":\"2026-10-21T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"0a8096bb8a8e7f98f3af2b891a19fe0dd4a2ffbc35b48dc66302d28116dcdcb00968e57a62b34ed78ec834675dfc4cdfb421df52415dacd7c0b8cc78524c130f\",\"signer_id\":\"did:key:z6MkvXS98H92X1Y2DMRmDsGrtMXhsNZvCp2vcnh8NtVApVxh\",\"signer_pubkey\":\"eeccba9faecd6f65af29ba7714605fe62d2195e6f031c7b8736f816d3eeeeb36\",\"subject\":\"e09627f6f863adc91750c6193680047d1688f5f718aaed7e745334fe1c1712b0\",\"subject_type\":\"object\"}", + "context": { + "publications": [ + "lecture_media" + ], + "room": "lecture", + "previous": null + }, + "outcome": { + "outcome": "accept", + "objects": [ + { + "object": "e09627f6f863adc91750c6193680047d1688f5f718aaed7e745334fe1c1712b0", + "outcome": "allowed" + } + ] + } + } + ], + "gates": [ + { + "name": "claim_ahead_served", + "description": "the crawler claimed the feature before requesting it", + "policy": { + "window": 0, + "require_allowed": false + }, + "agent": "crawler", + "served": [], + "claims": [ + "inference_allowed" + ], + "request": [ + "cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887" + ], + "outcome": { + "outcome": "serve" + } + }, + { + "name": "claim_ahead_unclaimed", + "description": "a request for an object no claim names", + "policy": { + "window": 0, + "require_allowed": false + }, + "agent": "crawler", + "served": [], + "claims": [ + "inference_allowed" + ], + "request": [ + "cf8bac17e7673c6b99f3854f4cc376ed1e168e47eae8f0e9316269122586e9d0" + ], + "outcome": { + "outcome": "refuse", + "reason": "claim_required" + } + }, + { + "name": "claim_ahead_disallowed", + "description": "a request for an object the crawler also claimed for a use the statement disallows", + "policy": { + "window": 0, + "require_allowed": false + }, + "agent": "crawler", + "served": [], + "claims": [ + "inference_allowed", + "training_forbidden" + ], + "request": [ + "cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887" + ], + "outcome": { + "outcome": "refuse", + "reason": "usage_disallowed" + } + }, + { + "name": "unknown_served", + "description": "an unknown preference is served when the policy does not require allowed", + "policy": { + "window": 0, + "require_allowed": false + }, + "agent": "crawler", + "served": [], + "claims": [ + "unknown_use" + ], + "request": [ + "8208d92f31db13460a23915c20213038a8391714ceae054f0dd8c72ba9299350" + ], + "outcome": { + "outcome": "serve" + } + }, + { + "name": "unknown_not_granted", + "description": "an unknown preference under a policy that requires allowed", + "policy": { + "window": 0, + "require_allowed": true + }, + "agent": "crawler", + "served": [], + "claims": [ + "inference_allowed", + "unknown_use" + ], + "request": [ + "8208d92f31db13460a23915c20213038a8391714ceae054f0dd8c72ba9299350" + ], + "outcome": { + "outcome": "refuse", + "reason": "usage_not_granted" + } + }, + { + "name": "claim_behind_within_window", + "description": "window 1: the last group served may be unclaimed", + "policy": { + "window": 1, + "require_allowed": false + }, + "agent": "crawler", + "served": [ + [ + "cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887" + ], + [ + "8208d92f31db13460a23915c20213038a8391714ceae054f0dd8c72ba9299350" + ] + ], + "claims": [ + "inference_allowed" + ], + "request": [ + "cf8bac17e7673c6b99f3854f4cc376ed1e168e47eae8f0e9316269122586e9d0" + ], + "outcome": { + "outcome": "serve" + } + }, + { + "name": "claim_behind_past_window", + "description": "window 1: an earlier group served and never claimed", + "policy": { + "window": 1, + "require_allowed": false + }, + "agent": "crawler", + "served": [ + [ + "cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887" + ], + [ + "8208d92f31db13460a23915c20213038a8391714ceae054f0dd8c72ba9299350" + ], + [ + "cf8bac17e7673c6b99f3854f4cc376ed1e168e47eae8f0e9316269122586e9d0" + ] + ], + "claims": [ + "inference_allowed" + ], + "request": [ + "3e0ea2c000e3b91fdc3705c72054c16ef27f008b23a9520104422fc887a6e2a4" + ], + "outcome": { + "outcome": "refuse", + "reason": "claim_required" + } + }, + { + "name": "another_agents_claim", + "description": "a claim signed by another agent is not held", + "policy": { + "window": 0, + "require_allowed": false + }, + "agent": "agent_a", + "served": [], + "claims": [ + "inference_allowed" + ], + "request": [ + "cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887" + ], + "outcome": { + "outcome": "refuse", + "reason": "claim_required" + } + }, + { + "name": "refused_claim_not_held", + "description": "a claim refused for another reason than the use is not held", + "policy": { + "window": 0, + "require_allowed": false + }, + "agent": "agent_d", + "served": [], + "claims": [ + "bad_signature" + ], + "request": [ + "cea863585f073b53e5e30013be89078bde7d1c42c710a49b1a4875a9d9fb2887" + ], + "outcome": { + "outcome": "refuse", + "reason": "claim_required" + } + }, + { + "name": "room_agent_served", + "description": "agent A claimed the lecture at 10 s under its grant; the relay requires allowed", + "policy": { + "window": 0, + "require_allowed": true + }, + "agent": "agent_a", + "served": [], + "claims": [ + "room_inference" + ], + "request": [ + "e09627f6f863adc91750c6193680047d1688f5f718aaed7e745334fe1c1712b0" + ], + "outcome": { + "outcome": "serve" + } + }, + { + "name": "room_agent_disallowed", + "description": "agent A's training claim over the lecture at 10 s", + "policy": { + "window": 0, + "require_allowed": true + }, + "agent": "agent_a", + "served": [], + "claims": [ + "room_inference", + "room_training_unlicensed" + ], + "request": [ + "e09627f6f863adc91750c6193680047d1688f5f718aaed7e745334fe1c1712b0" + ], + "outcome": { + "outcome": "refuse", + "reason": "usage_disallowed" + } + } + ] +} diff --git a/wai/wai-rs/Cargo.toml b/wai/wai-rs/Cargo.toml index 2f54cb110..0decf5626 100644 --- a/wai/wai-rs/Cargo.toml +++ b/wai/wai-rs/Cargo.toml @@ -255,6 +255,14 @@ live_watermark = ["live_room"] # wai.video.ssai's receipt: the decision recomputed, the asset list bound, the spend held to the # decider grant's ceiling. SHA-256, BLAKE3, Ed25519 and the key log only; no new dependency. live_ads = ["live_room", "video_ssai"] +# AI usage preferences and consumption claims (extensions/ai-usage): `wai.ai.usage` — the usage +# statement (draft-ietf-aipref-vocab-08's Structured Field form, the TDM Reservation Protocol's +# properties and the Training and Data Mining Assertion's entries, verbatim) in an envelope's +# manifest, a room's WAU1 record and a grant's terms; the HTTP and catalog fields checked +# against it; and the jwp-receipts `consumption` claim checked against its objects and room. +# RFC 9651's Dictionary parser is written here. SHA-256, Ed25519 and the room's records only; +# no new dependency. +ai_usage = ["live_room"] # Timed text and live captions (extensions/timed-text): `wai.text.timed` — the cue model, # live caption revisions (`caption` events of wai.live.events with the WTX1 annotation), # and their exact mappings onto WebVTT documents and segments, the IMSC1 Text Profile, @@ -719,6 +727,11 @@ name = "wai_live_ads_vectors" path = "src/bin/wai_live_ads_vectors.rs" required-features = ["live_ads"] +[[bin]] +name = "wai_ai_usage_vectors" +path = "src/bin/wai_ai_usage_vectors.rs" +required-features = ["ai_usage"] + [[bin]] name = "wai_timed_text_vectors" path = "src/bin/wai_timed_text_vectors.rs" @@ -1410,6 +1423,11 @@ name = "live_ads" path = "tests/live_ads.rs" required-features = ["live_ads"] +[[test]] +name = "ai_usage" +path = "tests/ai_usage.rs" +required-features = ["ai_usage"] + [[test]] name = "timed_text" path = "tests/timed_text.rs" diff --git a/wai/wai-rs/src/ai_usage.rs b/wai/wai-rs/src/ai_usage.rs new file mode 100644 index 000000000..426021858 --- /dev/null +++ b/wai/wai-rs/src/ai_usage.rs @@ -0,0 +1,992 @@ +//! AI usage preferences and consumption claims (`extensions/ai-usage.md`): the record +//! `wai.ai.usage`. +//! +//! A **usage statement** (§3) carries, verbatim, the preferences of three vocabularies: the +//! AI usage preference vocabulary of `draft-ietf-aipref-vocab-08` in its exemplary Structured +//! Field form, the TDM Reservation Protocol's `tdm-reservation` and `tdm-policy`, and the +//! entries of a `cawg.training-mining` assertion (version 1.1). It rides in an envelope's +//! manifest member `ai_usage`, signed by the object's receipt (§4.1); in a room usage record, +//! `WAU1`, signed by the room authority (§4.2); and in a room grant's attribute `ai_usage`, as +//! the terms the authority grants its principal (§4.3). HTTP fields (§4.4) and a MoQ catalog +//! field (§4.5) restate it unsigned, and are checked against it. +//! +//! [`preference`] is §5's one rule: a grant's terms, then the signed statements, the most +//! restrictive governing. A **consumption claim** (§6) is the jwp-receipts claim class +//! `consumption`, whose body and class rules are `crate::claim_receipt::consumption`'s; this +//! module checks it against its objects, their statements and its room (§6.3), and decides a +//! gate's request (§7). +//! +//! The corpus is `live-conformance/usage/` ([`corpus`]); its second implementation is +//! `live-conformance/ai_usage_verify.py`, which shares no code with this module. + +use crate::claim_receipt::{self, ClaimClass, ClaimReceipt, ConsumptionClaim, UsageEntry, Vocabulary}; +use crate::jwp_delivery::{GroupReceipt, ObjectReceipt}; +use crate::live_room::{self, Descriptor, Grant}; +use serde_json::{Map, Value}; +use sha2::{Digest, Sha256}; +use std::collections::{BTreeMap, BTreeSet}; + +pub mod corpus; +pub mod sf; + +/// The record this extension registers. +pub const CAPABILITY: &str = "wai.ai.usage"; +/// The room usage record's magic (§4.2). +pub const ROOM_MAGIC: [u8; 4] = *b"WAU1"; +/// The room usage record's signing domain (§4.2). +pub const ROOM_DOMAIN: &[u8] = b"wai:ai-usage-room\x01"; +/// The record version this revision reads. +pub const RECORD_VERSION: u8 = 1; +/// The manifest member (§4.1). +pub const MANIFEST_MEMBER: &str = "ai_usage"; +/// The grant attribute (§4.3). +pub const GRANT_ATTRIBUTE: &str = "ai_usage"; +/// The MoQ catalog field (§4.5; moq-streaming-format §4). +pub const CATALOG_FIELD: &str = "wai_ai_usage"; +/// The HTTP request field an agent gives a gateway a claim in (§7.3). +pub const CLAIM_FIELD: &str = "WAI-Usage-Claim"; +/// The revisions each vocabulary is read at (§3.2, §3.4, §3.5). +pub const AIPREF_REVISION: &str = "draft-ietf-aipref-vocab-08"; +pub const CAWG_REVISION: &str = "1.1"; +pub const TDMREP_REVISION: &str = "CG-FINAL-tdmrep-20240510"; +/// The longest canonical form of a statement (§3.6 step 3). +pub const MAX_STATEMENT: usize = 4096; +/// The longest `aipref` value (§3.2). +pub const MAX_AIPREF_VALUE: usize = 1024; +/// The most entries of a `cawg` statement (§3.4). +pub const MAX_CAWG_ENTRIES: usize = 32; +/// The longest gate window (§7.1). +pub const MAX_WINDOW: u8 = 16; + +// --------------------------------------------------------------------------------------- +// Reasons (§9) +// --------------------------------------------------------------------------------------- + +/// A refusal: a code of this extension's §9, a live-rooms code a room usage record or a grant +/// raises, or a jwp-receipts §3.1 code a claim raises at steps 1 to 5. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Reason { + UsageMalformed, + UsageRevisionUnknown, + ReceiptInvalid, + UsageUnbound, + RoomUsageUnlinked, + GrantUsageMalformed, + HeaderConflicts, + CatalogConflicts, + ConsumptionObjectUnknown, + GrantMismatch, + PreferenceMismatch, + UsageUnanswered, + UsageDisallowed, + ClaimRequired, + UsageNotGranted, + /// A live-rooms code: a room usage record that does not read or verify (§4.2), a + /// descriptor or a grant the room refuses. + Room(live_room::Reason), + /// A jwp-receipts §3.1 code, among them the six of ai-usage §6.2. + Claim(claim_receipt::Reason), +} + +/// The codes §9 lists, in its order: this extension's own, then the class rules of §6.2 that +/// `claim_receipt` raises. +pub const CODES: [&str; 21] = [ + "usage_malformed", + "usage_revision_unknown", + "receipt_invalid", + "usage_unbound", + "room_usage_unlinked", + "grant_usage_malformed", + "header_conflicts", + "catalog_conflicts", + "consumption_objects_unordered", + "consumption_subject_mismatch", + "consumption_usage_unordered", + "consumption_term_unknown", + "consumption_seen_unordered", + "consumption_signer_not_principal", + "consumption_object_unknown", + "grant_mismatch", + "preference_mismatch", + "usage_unanswered", + "usage_disallowed", + "claim_required", + "usage_not_granted", +]; + +impl Reason { + pub fn code(self) -> &'static str { + match self { + Reason::UsageMalformed => "usage_malformed", + Reason::UsageRevisionUnknown => "usage_revision_unknown", + Reason::ReceiptInvalid => "receipt_invalid", + Reason::UsageUnbound => "usage_unbound", + Reason::RoomUsageUnlinked => "room_usage_unlinked", + Reason::GrantUsageMalformed => "grant_usage_malformed", + Reason::HeaderConflicts => "header_conflicts", + Reason::CatalogConflicts => "catalog_conflicts", + Reason::ConsumptionObjectUnknown => "consumption_object_unknown", + Reason::GrantMismatch => "grant_mismatch", + Reason::PreferenceMismatch => "preference_mismatch", + Reason::UsageUnanswered => "usage_unanswered", + Reason::UsageDisallowed => "usage_disallowed", + Reason::ClaimRequired => "claim_required", + Reason::UsageNotGranted => "usage_not_granted", + Reason::Room(r) => r.code(), + Reason::Claim(r) => r.label(), + } + } +} + +fn sha256(parts: &[&[u8]]) -> [u8; 32] { + let mut h = Sha256::new(); + for p in parts { + h.update(p); + } + h.finalize().into() +} + +fn hex(b: &[u8]) -> String { + claim_receipt::hex(b) +} + +/// A live-events code, as a room raises it (live-rooms §11). +fn ev(r: crate::live_events::Reason) -> Reason { + Reason::Room(live_room::Reason::Events(r)) +} + +// --------------------------------------------------------------------------------------- +// §3 The usage statement +// --------------------------------------------------------------------------------------- + +/// What a vocabulary says about one term (§2). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Preference { + Allow, + Disallow, + Unknown, +} + +impl Preference { + pub fn label(self) -> &'static str { + match self { + Preference::Allow => "allow", + Preference::Disallow => "disallow", + Preference::Unknown => "unknown", + } + } +} + +/// A `cawg` entry's `use` (§3.4). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CawgUse { + Allowed, + NotAllowed, + Constrained, +} + +impl CawgUse { + fn from_label(s: &str) -> Option { + match s { + "allowed" => Some(CawgUse::Allowed), + "notAllowed" => Some(CawgUse::NotAllowed), + "constrained" => Some(CawgUse::Constrained), + _ => None, + } + } +} + +/// A statement that reads (§3.6). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Statement { + /// `aipref`: the value, and each label's preference. + pub aipref: Option<(String, [Preference; 3])>, + /// `cawg`: each entry's use. + pub cawg: Option>, + /// `tdmrep`: the reservation (0 or 1) and the policy URL. + pub tdmrep: Option<(u8, Option)>, + /// The canonical form (§2). + pub canonical: String, +} + +/// The preferences of `draft-ietf-aipref-vocab-08` §6.5 for the three labels, in +/// [`claim_receipt::consumption::AIPREF_LABELS`]' order; `None` when the value does not parse. +pub fn aipref_preferences(value: &str) -> Option<[Preference; 3]> { + let d = sf::parse_dictionary(value)?; + Some(claim_receipt::consumption::AIPREF_LABELS.map(|l| match d.get(l) { + Some(sf::Member::Token(t)) if t == "y" => Preference::Allow, + Some(sf::Member::Token(t)) if t == "n" => Preference::Disallow, + _ => Preference::Unknown, + })) +} + +fn exact_members(m: &Map, allowed: &[&str], required: &[&str]) -> bool { + required.iter().all(|k| m.contains_key(*k)) && m.keys().all(|k| allowed.contains(&k.as_str())) +} + +fn policy_url_ok(s: &str) -> bool { + (8..=2048).contains(&s.len()) + && s.bytes().all(|b| (0x21..=0x7e).contains(&b)) + && (s.starts_with("https://") || s.starts_with("http://")) +} + +impl Statement { + /// §3.6 on a JSON value already read with no repeated member name. + pub fn read(v: &Value) -> Result { + let bad = Reason::UsageMalformed; + let m = v.as_object().ok_or(bad)?; + if m.is_empty() || m.keys().any(|k| !["aipref", "cawg", "tdmrep"].contains(&k.as_str())) { + return Err(bad); + } + // Step 1, member by member in §3.1's order; the revisions are only typed here. + let mut revisions: Vec<(&str, &str)> = Vec::new(); + let mut aipref = None; + if let Some(a) = m.get("aipref") { + let a = a.as_object().ok_or(bad)?; + if !exact_members(a, &["revision", "value"], &["revision", "value"]) { + return Err(bad); + } + let rev = a["revision"].as_str().ok_or(bad)?; + let value = a["value"].as_str().ok_or(bad)?; + if value.is_empty() || value.len() > MAX_AIPREF_VALUE || !value.is_ascii() { + return Err(bad); + } + let prefs = aipref_preferences(value).ok_or(bad)?; + revisions.push((rev, AIPREF_REVISION)); + aipref = Some((value.to_owned(), prefs)); + } + let mut cawg = None; + if let Some(c) = m.get("cawg") { + let c = c.as_object().ok_or(bad)?; + if !exact_members(c, &["revision", "entries"], &["revision", "entries"]) { + return Err(bad); + } + let rev = c["revision"].as_str().ok_or(bad)?; + let e = c["entries"].as_object().ok_or(bad)?; + if e.is_empty() || e.len() > MAX_CAWG_ENTRIES { + return Err(bad); + } + let mut entries = BTreeMap::new(); + for (k, v) in e { + let reserved_ok = !k.starts_with("cawg.") || claim_receipt::consumption::CAWG_KEYS.contains(&k.as_str()); + if !claim_receipt::consumption::cawg_key_form(k) || !reserved_ok { + return Err(bad); + } + let v = v.as_object().ok_or(bad)?; + if !exact_members(v, &["use", "constraint_info"], &["use"]) { + return Err(bad); + } + let u = CawgUse::from_label(v["use"].as_str().ok_or(bad)?).ok_or(bad)?; + if let Some(ci) = v.get("constraint_info") { + let ci = ci.as_str().ok_or(bad)?; + if ci.is_empty() || ci.len() > 1024 { + return Err(bad); + } + } + entries.insert(k.clone(), u); + } + revisions.push((rev, CAWG_REVISION)); + cawg = Some(entries); + } + let mut tdmrep = None; + if let Some(t) = m.get("tdmrep") { + let t = t.as_object().ok_or(bad)?; + let names = ["revision", "tdm-reservation", "tdm-policy"]; + if !exact_members(t, &names, &names) { + return Err(bad); + } + let rev = t["revision"].as_str().ok_or(bad)?; + let reservation = match t["tdm-reservation"].as_u64() { + Some(0) => 0, + Some(1) => 1, + _ => return Err(bad), + }; + let policy = match &t["tdm-policy"] { + Value::Null => None, + Value::String(s) if policy_url_ok(s) => Some(s.clone()), + _ => return Err(bad), + }; + revisions.push((rev, TDMREP_REVISION)); + tdmrep = Some((reservation, policy)); + } + // Step 2. + if revisions.iter().any(|(got, want)| got != want) { + return Err(Reason::UsageRevisionUnknown); + } + // Step 3. + let canonical = crate::jcs::jcs(v).map_err(|_| bad)?; + if canonical.len() > MAX_STATEMENT { + return Err(bad); + } + Ok(Statement { aipref, cawg, tdmrep, canonical }) + } + + /// §3.6 on JSON text: no member name repeated in an object, then [`Statement::read`]. + pub fn read_text(text: &str) -> Result { + let v = crate::claim_text::read(text).map_err(|_| Reason::UsageMalformed)?; + Statement::read(&v) + } + + /// §4.2 and §4.3: the bytes must read and be the canonical form of what they hold. + pub fn read_canonical(bytes: &[u8]) -> Result { + let text = std::str::from_utf8(bytes).map_err(|_| Reason::UsageMalformed)?; + let s = Statement::read_text(text)?; + if s.canonical.as_bytes() != bytes { + return Err(Reason::UsageMalformed); + } + Ok(s) + } + + /// The digest (§3.6). + pub fn digest(&self) -> [u8; 32] { + sha256(&[self.canonical.as_bytes()]) + } + + /// The vocabularies it carries, in order. + pub fn vocabularies(&self) -> Vec { + let mut v = Vec::new(); + if self.aipref.is_some() { + v.push(Vocabulary::Aipref); + } + if self.cawg.is_some() { + v.push(Vocabulary::Cawg); + } + if self.tdmrep.is_some() { + v.push(Vocabulary::Tdmrep); + } + v + } + + /// The preference it gives `term` of `vocabulary` (§3.2, §3.4, §3.5); `None` when it does + /// not carry the vocabulary. + pub fn preference(&self, vocabulary: Vocabulary, term: &str) -> Option { + match vocabulary { + Vocabulary::Aipref => self.aipref.as_ref().map(|(_, p)| { + claim_receipt::consumption::AIPREF_LABELS.iter().position(|l| *l == term).map_or(Preference::Unknown, |i| p[i]) + }), + Vocabulary::Cawg => self.cawg.as_ref().map(|e| match e.get(term) { + Some(CawgUse::Allowed) => Preference::Allow, + Some(CawgUse::NotAllowed | CawgUse::Constrained) => Preference::Disallow, + None => Preference::Unknown, + }), + Vocabulary::Tdmrep => self.tdmrep.as_ref().map(|(r, _)| { + if term != claim_receipt::consumption::TDM_TERM { + Preference::Unknown + } else if *r == 1 { + Preference::Disallow + } else { + Preference::Allow + } + }), + } + } + + /// Every term this statement names, with its preference: the three `aipref` labels, the + /// `cawg` keys and `tdm-reservation`. For reports. + pub fn terms(&self) -> Vec<(Vocabulary, String, Preference)> { + let mut out = Vec::new(); + if let Some((_, p)) = &self.aipref { + for (l, pref) in claim_receipt::consumption::AIPREF_LABELS.iter().zip(p) { + out.push((Vocabulary::Aipref, (*l).to_owned(), *pref)); + } + } + if let Some(e) = &self.cawg { + for k in e.keys() { + out.push((Vocabulary::Cawg, k.clone(), self.preference(Vocabulary::Cawg, k).expect("carried"))); + } + } + if self.tdmrep.is_some() { + let t = claim_receipt::consumption::TDM_TERM; + out.push((Vocabulary::Tdmrep, t.to_owned(), self.preference(Vocabulary::Tdmrep, t).expect("carried"))); + } + out + } +} + +// --------------------------------------------------------------------------------------- +// §4.1 The envelope +// --------------------------------------------------------------------------------------- + +/// An object a verifier holds (§4.1). +#[derive(Clone, Debug)] +pub struct HeldObject { + pub content_hash: [u8; 32], + /// Its statement, signed by `signer`, or none. + pub statement: Option, + /// The group receipt's `signer_pubkey`. + pub signer: [u8; 32], + /// Its manifest's `live.t`, where it has an integer one. + pub t: Option, +} + +/// The manifest of a `WAI1` or `WAI2` envelope as a JSON value, read as SPEC §2 reads it. +fn manifest_value(envelope: &[u8]) -> Option { + let text = crate::container::manifest_bytes(envelope).ok()?; + crate::container::check_manifest_text(text).ok()?; + serde_json::from_slice(text).ok() +} + +/// §4.1: accept the receipts, require the envelope to hash to the object receipt's content +/// hash, and read the manifest's `ai_usage`. +pub fn hold_object(envelope: &[u8], object: &ObjectReceipt, group: &GroupReceipt) -> Result { + group.verify().map_err(|_| Reason::ReceiptInvalid)?; + object.verify_inclusion(group).map_err(|_| Reason::ReceiptInvalid)?; + let h = crate::jwp_hash::content_hash(envelope); + if h != object.content_hash { + return Err(Reason::UsageUnbound); + } + let m = manifest_value(envelope).ok_or(Reason::UsageMalformed)?; + let statement = match m.get(MANIFEST_MEMBER) { + None => None, + Some(v) => Some(Statement::read(v)?), + }; + let t = m.get("live").and_then(|l| l.get("t")).and_then(Value::as_u64); + Ok(HeldObject { content_hash: h, statement, signer: group.signer_pubkey, t }) +} + +// --------------------------------------------------------------------------------------- +// §4.2 The room usage record, §4.3 a grant's terms +// --------------------------------------------------------------------------------------- + +/// A room usage record (§4.2). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RoomRecord { + pub room: [u8; 32], + pub prev: [u8; 32], + pub effective: u64, + pub statement: Statement, + pub sig: [u8; 64], +} + +impl RoomRecord { + /// The bytes from `magic` through `statement`. + pub fn body(&self) -> Vec { + let mut o = Vec::new(); + o.extend_from_slice(&ROOM_MAGIC); + o.push(RECORD_VERSION); + o.extend_from_slice(&self.room); + o.extend_from_slice(&self.prev); + o.extend_from_slice(&self.effective.to_le_bytes()); + let s = self.statement.canonical.as_bytes(); + o.extend_from_slice(&(s.len() as u16).to_le_bytes()); + o.extend_from_slice(s); + o + } + + pub fn to_bytes(&self) -> Vec { + let mut o = self.body(); + o.extend_from_slice(&self.sig); + o + } + + pub fn digest(&self) -> [u8; 32] { + sha256(&[&self.body()]) + } + + pub fn sign(mut self, authority: &ed25519_dalek::SigningKey) -> RoomRecord { + use ed25519_dalek::Signer; + let mut m = ROOM_DOMAIN.to_vec(); + m.extend_from_slice(&self.body()); + self.sig = authority.sign(&m).to_bytes(); + self + } + + /// §4.2's reading: the layout, then the statement. Not the room, the signature or the + /// chain, which need the room. + pub fn read(b: &[u8]) -> Result { + use crate::live_events::Reason as L; + if b.len() < 4 { + return Err(ev(L::Truncated)); + } + if b[..4] != ROOM_MAGIC { + return Err(ev(L::Malformed)); + } + if b.len() < 5 { + return Err(ev(L::Truncated)); + } + if b[4] != RECORD_VERSION { + return Err(ev(L::UnknownVersion)); + } + let take = |p: &mut usize, n: usize| -> Result<&[u8], Reason> { + let s = b.get(*p..*p + n).ok_or(ev(L::Truncated))?; + *p += n; + Ok(s) + }; + let mut p = 5; + let room_id: [u8; 32] = take(&mut p, 32)?.try_into().expect("32"); + let prev: [u8; 32] = take(&mut p, 32)?.try_into().expect("32"); + let effective = u64::from_le_bytes(take(&mut p, 8)?.try_into().expect("8")); + let n = u16::from_le_bytes(take(&mut p, 2)?.try_into().expect("2")) as usize; + let statement_bytes = take(&mut p, n)?.to_vec(); + let sig: [u8; 64] = take(&mut p, 64)?.try_into().expect("64"); + if p != b.len() { + return Err(ev(L::Malformed)); + } + let statement = Statement::read_canonical(&statement_bytes)?; + Ok(RoomRecord { room: room_id, prev, effective, statement, sig }) + } +} + +/// A room as §4.2 and §4.3 check it. +#[derive(Clone, Debug)] +pub struct CheckedRoom { + pub descriptor: Descriptor, + pub id: [u8; 32], + /// The grants live-rooms §4.3 accepts, each with its terms: `Some(Ok)` when it carries + /// terms that read, `Some(Err)` when they do not, `None` when it carries none. + pub grants: Vec<(live_room::HeldGrant, Option>)>, + /// The usage records, or the first refused and its index (§4.2). + pub records: Result, (usize, Reason)>, + /// The grants' refusal, when live-rooms §4.3 refuses one: its index and reason. + pub grants_refused: Option<(usize, Reason)>, +} + +impl CheckedRoom { + /// The records in effect at `t`: the one in effect (§4.2), or every record where `t` is + /// `None`. None when the records were refused: such a room states no preference. + pub fn records_at(&self, t: Option) -> Vec<&RoomRecord> { + let Ok(records) = &self.records else { return Vec::new() }; + match t { + None => records.iter().collect(), + Some(t) => records.iter().rev().find(|r| r.effective <= t).into_iter().collect(), + } + } + + /// The grant of this digest that the room accepts. + pub fn grant(&self, digest: &[u8; 32]) -> Option<&(live_room::HeldGrant, Option>)> { + self.grants.iter().find(|(h, _)| h.digest == *digest) + } +} + +/// A grant's terms (§4.3): `None` when it carries no `ai_usage` attribute. +pub fn grant_terms(g: &Grant) -> Option> { + let (_, v) = g.attrs.iter().find(|(k, _)| k == GRANT_ATTRIBUTE)?; + Some(Statement::read_canonical(v.as_bytes()).map_err(|_| Reason::GrantUsageMalformed)) +} + +/// Check a room (§4.2, §4.3): its descriptor, which must verify (`Err` otherwise), its grants +/// and its usage records. +pub fn check_room(descriptor: &[u8], grants: &[Vec], records: &[Vec]) -> Result { + let d = Descriptor::parse(descriptor).map_err(Reason::Room)?; + let id = d.room_id(); + let (held, grants_refused) = match live_room::check_grants(&d, &id, grants) { + Ok(h) => (h, None), + Err((i, r)) => (Vec::new(), Some((i, Reason::Room(r)))), + }; + let grants = held.into_iter().map(|h| { + let terms = grant_terms(&h.grant); + (h, terms) + }); + let records = check_records(&d, &id, records); + Ok(CheckedRoom { descriptor: d.clone(), id, grants: grants.collect(), records, grants_refused }) +} + +/// §4.2 over a room's records, in order. +pub fn check_records(d: &Descriptor, id: &[u8; 32], records: &[Vec]) -> Result, (usize, Reason)> { + let mut out: Vec = Vec::new(); + for (i, b) in records.iter().enumerate() { + let r = RoomRecord::read(b).map_err(|e| (i, e))?; + if r.room != *id { + return Err((i, ev(crate::live_events::Reason::WrongRoom))); + } + let mut m = ROOM_DOMAIN.to_vec(); + m.extend_from_slice(&b[..b.len() - 64]); + crate::sig::verify_strict(&d.authority, &m, &r.sig).map_err(|e| { + ( + i, + ev(match e { + crate::sig::SigError::BadKey => crate::live_events::Reason::BadKey, + crate::sig::SigError::BadSignature => crate::live_events::Reason::BadSignature, + }), + ) + })?; + let linked = match out.last() { + None => r.prev == [0; 32] && r.effective == 0, + Some(last) => r.prev == last.digest() && r.effective > last.effective, + }; + if !linked { + return Err((i, Reason::RoomUsageUnlinked)); + } + out.push(r); + } + Ok(out) +} + +// --------------------------------------------------------------------------------------- +// §4.4 HTTP, §4.5 the MoQ catalog +// --------------------------------------------------------------------------------------- + +/// §4.4: compare a response's fields with the object's signed statement. `Ok` holds the +/// vocabularies the fields state unsigned: those the signed statement does not carry. +pub fn check_fields(signed: Option<&Statement>, fields: &[(String, String)]) -> Result, Reason> { + let lines = |name: &str| -> Vec<&str> { + fields.iter().filter(|(n, _)| n.eq_ignore_ascii_case(name)).map(|(_, v)| v.as_str()).collect() + }; + let trim = |s: &str| s.trim_matches(|c| c == ' ' || c == '\t').to_owned(); + let mut unsigned = Vec::new(); + let usage = lines("Content-Usage"); + if !usage.is_empty() { + match signed.and_then(|s| s.aipref.as_ref()) { + None => unsigned.push(Vocabulary::Aipref), + Some((_, prefs)) => { + let combined = usage.join(", "); + if aipref_preferences(&combined).as_ref() != Some(prefs) { + return Err(Reason::HeaderConflicts); + } + } + } + } + let reservation = lines("tdm-reservation"); + let policy = lines("tdm-policy"); + if !reservation.is_empty() || !policy.is_empty() { + match signed.and_then(|s| s.tdmrep.as_ref()) { + None => unsigned.push(Vocabulary::Tdmrep), + Some((r, p)) => { + if reservation.len() > 1 || policy.len() > 1 { + return Err(Reason::HeaderConflicts); + } + if let Some(v) = reservation.first() { + if trim(v) != r.to_string() { + return Err(Reason::HeaderConflicts); + } + } + if let Some(v) = policy.first() { + if Some(trim(v)) != *p { + return Err(Reason::HeaderConflicts); + } + } + } + } + } + Ok(unsigned) +} + +/// §4.5: a catalog track entry against the held objects of its track. +pub fn check_catalog(entry_text: &str, objects: &[&HeldObject]) -> Result<(), Reason> { + let v = crate::claim_text::read(entry_text).map_err(|_| Reason::CatalogConflicts)?; + let Some(s) = v.get(CATALOG_FIELD) else { return Ok(()) }; + let s = Statement::read(s).map_err(|_| Reason::CatalogConflicts)?; + if objects.iter().filter_map(|o| o.statement.as_ref()).any(|o| o.digest() != s.digest()) { + return Err(Reason::CatalogConflicts); + } + Ok(()) +} + +// --------------------------------------------------------------------------------------- +// §5 Precedence +// --------------------------------------------------------------------------------------- + +/// An object's governing statements (§5.1): its own, then the room's records in effect at its +/// time, when it is the room's. +pub fn governing<'a>(o: &'a HeldObject, room: Option<&'a CheckedRoom>) -> Vec<&'a Statement> { + let mut v: Vec<&Statement> = o.statement.iter().collect(); + if let Some(r) = room { + v.extend(r.records_at(o.t).into_iter().map(|r| &r.statement)); + } + v +} + +/// §5.2 for one use: `None` when it is outside the vocabulary (`term` is `None`). +pub fn preference(entry: &UsageEntry, governing: &[&Statement], terms: Option<&Statement>) -> Option { + let term = entry.term.as_deref()?; + if let Some(p) = terms.and_then(|t| t.preference(entry.vocabulary, term)) { + if p != Preference::Unknown { + return Some(p); + } + } + let ps: Vec = governing.iter().filter_map(|s| s.preference(entry.vocabulary, term)).collect(); + Some(if ps.contains(&Preference::Disallow) { + Preference::Disallow + } else if ps.contains(&Preference::Allow) { + Preference::Allow + } else { + Preference::Unknown + }) +} + +/// What an accepted claim states for one object (§6.4). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Outcome { + Allowed, + Unknown, + Outside, +} + +impl Outcome { + pub fn label(self) -> &'static str { + match self { + Outcome::Allowed => "allowed", + Outcome::Unknown => "unknown", + Outcome::Outside => "outside", + } + } +} + +/// §5.2's last paragraph over a claim's entries for one object: `Err` on a disallow. +fn combine(prefs: &[Option]) -> Result { + if prefs.contains(&Some(Preference::Disallow)) { + Err(()) + } else if prefs.contains(&Some(Preference::Allow)) { + Ok(Outcome::Allowed) + } else if prefs.iter().all(Option::is_none) { + Ok(Outcome::Outside) + } else { + Ok(Outcome::Unknown) + } +} + +// --------------------------------------------------------------------------------------- +// §6 The consumption claim +// --------------------------------------------------------------------------------------- + +/// What a claim is checked against (§6.3, §8). +pub struct ClaimContext<'a> { + /// The objects the verifier holds, by content hash, and whether each is the room's. + pub objects: &'a BTreeMap<[u8; 32], (HeldObject, bool)>, + /// The room, where the claim's objects are a room's or it names a grant. + pub room: Option<&'a CheckedRoom>, + /// The claim before it of its signer, as the verifier holds it. + pub previous: Option<&'a ClaimReceipt>, +} + +/// An accepted claim: the claim, and each object's outcome in the order of `objects`. +#[derive(Clone, Debug)] +pub struct Accepted { + pub claim: ClaimReceipt, + pub outcomes: Vec<([u8; 32], Outcome)>, +} + +/// A refused claim: the jwp-receipts §3.1 step, the reason, and the claim where it read. +#[derive(Clone, Debug)] +pub struct Refused { + pub step: u8, + pub reason: Reason, + pub claim: Option, +} + +fn body(c: &ClaimReceipt) -> Option<&ConsumptionClaim> { + match &c.body { + claim_receipt::ClaimBody::Consumption(b) => Some(b), + _ => None, + } +} + +/// Verify a consumption claim's wire form: jwp-receipts §3.1 steps 1 to 5, then §6.3. +pub fn verify_claim(wire: &str, ctx: &ClaimContext<'_>) -> Result { + let refuse = |step: u8, reason: Reason, claim: Option<&ClaimReceipt>| Refused { step, reason, claim: claim.cloned() }; + let claim = claim_receipt::verify_claim_json(wire).map_err(|r| refuse(r.step, Reason::Claim(r.reason), None))?; + claim.check_parent(ctx.previous).map_err(|r| refuse(r.step, Reason::Claim(r.reason), Some(&claim)))?; + let Some(b) = body(&claim) else { + return Err(refuse(6, Reason::Claim(claim_receipt::Reason::ContextMismatch), Some(&claim))); + }; + let at6 = |reason: Reason| refuse(6, reason, Some(&claim)); + // Step 1. + let objects: Vec<&(HeldObject, bool)> = + b.objects.iter().map(|h| ctx.objects.get(h)).collect::>().ok_or_else(|| at6(Reason::ConsumptionObjectUnknown))?; + // Step 2. + let mut terms: Option<&Statement> = None; + if let Some(g) = &b.grant { + let room = ctx.room.ok_or_else(|| at6(Reason::GrantMismatch))?; + let (held, t) = room.grant(g).ok_or_else(|| at6(Reason::GrantMismatch))?; + let good = held.grant.principal == claim.signer_pubkey + && objects.iter().all(|(o, of_room)| { + *of_room && o.t.is_none_or(|t| held.grant.covers(room.descriptor.t0_us, t)) + }); + if !good { + return Err(at6(Reason::GrantMismatch)); + } + terms = t.as_ref().and_then(|t| t.as_ref().ok()); + } + let room_of = |of_room: bool| if of_room { ctx.room } else { None }; + // Step 3. + let seen: BTreeSet<[u8; 32]> = + objects.iter().flat_map(|(o, r)| governing(o, room_of(*r)).into_iter().map(Statement::digest)).collect(); + if seen.iter().copied().collect::>() != b.seen { + return Err(at6(Reason::PreferenceMismatch)); + } + // Step 4. + let carried: BTreeSet = + objects.iter().flat_map(|(o, r)| governing(o, room_of(*r)).into_iter().flat_map(Statement::vocabularies)).collect(); + let answered: BTreeSet = b.usage.iter().map(|u| u.vocabulary).collect(); + if !carried.is_subset(&answered) { + return Err(at6(Reason::UsageUnanswered)); + } + // Step 5. + let mut outcomes = Vec::new(); + for (h, (o, r)) in b.objects.iter().zip(&objects) { + let g = governing(o, room_of(*r)); + let prefs: Vec> = b.usage.iter().map(|u| preference(u, &g, terms)).collect(); + let outcome = combine(&prefs).map_err(|_| at6(Reason::UsageDisallowed))?; + outcomes.push((*h, outcome)); + } + Ok(Accepted { claim: claim.clone(), outcomes }) +} + +// --------------------------------------------------------------------------------------- +// §7 The gate +// --------------------------------------------------------------------------------------- + +/// A gate's policy (§7.1). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Policy { + pub window: u8, + pub require_allowed: bool, +} + +/// §7.2: decide a request. `claims` are the claims the agent gave, each as verified; `served` +/// the groups served the agent, in order; `request` the group requested. +pub fn gate( + policy: Policy, + agent: &[u8; 32], + served: &[Vec<[u8; 32]>], + claims: &[Result], + request: &[[u8; 32]], +) -> Result<(), Reason> { + // The held claims: the agent's, accepted or refused `usage_disallowed` at step 6. + let mut allowed: Vec<(&ClaimReceipt, &[([u8; 32], Outcome)])> = Vec::new(); + let mut disallowing: Vec<&ClaimReceipt> = Vec::new(); + for c in claims { + match c { + Ok(a) if a.claim.signer_pubkey == *agent => allowed.push((&a.claim, &a.outcomes)), + Err(Refused { step: 6, reason: Reason::UsageDisallowed, claim: Some(c) }) if c.signer_pubkey == *agent => { + disallowing.push(c) + } + _ => {} + } + } + let names = |c: &ClaimReceipt, h: &[u8; 32]| body(c).is_some_and(|b| b.objects.contains(h)); + let checked: Vec<[u8; 32]> = if policy.window == 0 { + request.to_vec() + } else { + let keep = served.len().saturating_sub(policy.window as usize); + served[..keep].iter().flatten().copied().collect() + }; + for h in &checked { + let any_allowed = allowed.iter().any(|(c, _)| names(c, h)); + let any_disallowing = disallowing.iter().any(|c| names(c, h)); + if !any_allowed && !any_disallowing { + return Err(Reason::ClaimRequired); + } + if any_disallowing { + return Err(Reason::UsageDisallowed); + } + if policy.require_allowed + && allowed.iter().any(|(_, outs)| outs.iter().any(|(o, out)| o == h && *out != Outcome::Allowed)) + { + return Err(Reason::UsageNotGranted); + } + } + Ok(()) +} + +/// The class this module verifies. +pub const CLAIM_CLASS: ClaimClass = ClaimClass::Consumption; + +/// A report of a statement, as the corpus states it: the digest and every term's preference. +pub fn statement_report(s: &Statement) -> Value { + let mut m = Map::new(); + m.insert("digest".into(), hex(&s.digest()).into()); + let mut terms = Map::new(); + for (v, t, p) in s.terms() { + let e = terms.entry(v.label()).or_insert_with(|| Value::Object(Map::new())); + e.as_object_mut().expect("object").insert(t, p.label().into()); + } + m.insert("preferences".into(), Value::Object(terms)); + Value::Object(m) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn st(v: Value) -> Result { + Statement::read(&v) + } + + #[test] + fn a_statement_reads_its_three_vocabularies() { + let s = st(json!({ + "aipref": {"revision": AIPREF_REVISION, "value": "train-ai=n, search=y"}, + "cawg": {"revision": "1.1", "entries": {"cawg.ai_inference": {"use": "allowed"}, "cawg.data_mining": {"use": "constrained", "constraint_info": "x"}}}, + "tdmrep": {"revision": TDMREP_REVISION, "tdm-reservation": 1, "tdm-policy": "https://policy.example/tdm.json"} + })) + .unwrap(); + assert_eq!(s.preference(Vocabulary::Aipref, "train-ai"), Some(Preference::Disallow)); + assert_eq!(s.preference(Vocabulary::Aipref, "search"), Some(Preference::Allow)); + assert_eq!(s.preference(Vocabulary::Aipref, "ai-use"), Some(Preference::Unknown)); + assert_eq!(s.preference(Vocabulary::Cawg, "cawg.ai_inference"), Some(Preference::Allow)); + assert_eq!(s.preference(Vocabulary::Cawg, "cawg.data_mining"), Some(Preference::Disallow)); + assert_eq!(s.preference(Vocabulary::Cawg, "cawg.ai_training"), Some(Preference::Unknown)); + assert_eq!(s.preference(Vocabulary::Tdmrep, "tdm-reservation"), Some(Preference::Disallow)); + assert_eq!(s.vocabularies(), Vocabulary::ALL.to_vec()); + } + + #[test] + fn a_statement_is_refused_in_its_steps_order() { + let bad = |v: Value| st(v).unwrap_err(); + assert_eq!(bad(json!({})), Reason::UsageMalformed); + assert_eq!(bad(json!({"aipref": {"revision": AIPREF_REVISION, "value": "Train-AI=y"}})), Reason::UsageMalformed); + assert_eq!(bad(json!({"aipref": {"revision": "draft-ietf-aipref-vocab-07", "value": "train-ai=y"}})), Reason::UsageRevisionUnknown); + // A malformed vocabulary is step 1, before an unknown revision (step 2). + assert_eq!( + bad(json!({"aipref": {"revision": "x", "value": "train-ai=y"}, "tdmrep": {"revision": TDMREP_REVISION, "tdm-reservation": 2, "tdm-policy": null}})), + Reason::UsageMalformed + ); + assert_eq!(bad(json!({"tdmrep": {"revision": TDMREP_REVISION, "tdm-reservation": 1}})), Reason::UsageMalformed); + assert_eq!(bad(json!({"cawg": {"revision": "1.1", "entries": {"cawg.ai_summaries": {"use": "allowed"}}}})), Reason::UsageMalformed); + assert_eq!(bad(json!({"cawg": {"revision": "1.1", "entries": {"x": {"use": "maybe"}}}})), Reason::UsageMalformed); + assert_eq!(bad(json!({"aipref": {"revision": AIPREF_REVISION, "value": "a=1", "x": 1}})), Reason::UsageMalformed); + assert_eq!(Statement::read_text(r#"{"aipref":{"revision":"x","revision":"y","value":"a=1"}}"#).unwrap_err(), Reason::UsageMalformed); + } + + #[test] + fn the_rule_is_terms_then_the_most_restrictive_signed() { + let allow = st(json!({"aipref": {"revision": AIPREF_REVISION, "value": "train-ai=y"}})).unwrap(); + let deny = st(json!({"aipref": {"revision": AIPREF_REVISION, "value": "train-ai=n"}})).unwrap(); + let e = UsageEntry { vocabulary: Vocabulary::Aipref, term: Some("train-ai".into()) }; + assert_eq!(preference(&e, &[&allow, &deny], None), Some(Preference::Disallow)); + assert_eq!(preference(&e, &[&allow], None), Some(Preference::Allow)); + assert_eq!(preference(&e, &[], None), Some(Preference::Unknown)); + assert_eq!(preference(&e, &[&allow, &deny], Some(&allow)), Some(Preference::Allow), "terms lift a disallow"); + assert_eq!(preference(&e, &[&allow], Some(&deny)), Some(Preference::Disallow), "and narrow an allow"); + let outside = UsageEntry { vocabulary: Vocabulary::Aipref, term: None }; + assert_eq!(preference(&outside, &[&deny], None), None); + assert_eq!(combine(&[None, None]), Ok(Outcome::Outside)); + assert_eq!(combine(&[None, Some(Preference::Unknown)]), Ok(Outcome::Unknown)); + assert_eq!(combine(&[Some(Preference::Allow), Some(Preference::Unknown)]), Ok(Outcome::Allowed)); + assert_eq!(combine(&[Some(Preference::Allow), Some(Preference::Disallow)]), Err(())); + } + + #[test] + fn fields_conflict_by_meaning_not_spelling() { + let s = st(json!({"aipref": {"revision": AIPREF_REVISION, "value": "train-ai=n, search=y"}, + "tdmrep": {"revision": TDMREP_REVISION, "tdm-reservation": 1, "tdm-policy": null}})) + .unwrap(); + let f = |v: &[(&str, &str)]| v.iter().map(|(a, b)| (a.to_string(), b.to_string())).collect::>(); + assert_eq!(check_fields(Some(&s), &f(&[("content-usage", "search=y;x=1,train-ai=n, foo=y")])), Ok(vec![])); + assert_eq!(check_fields(Some(&s), &f(&[("Content-Usage", "train-ai=n"), ("Content-Usage", "search=y")])), Ok(vec![])); + assert_eq!(check_fields(Some(&s), &f(&[("Content-Usage", "train-ai=y, search=y")])), Err(Reason::HeaderConflicts)); + assert_eq!(check_fields(Some(&s), &f(&[("Content-Usage", "train-ai=n")])), Err(Reason::HeaderConflicts)); + assert_eq!(check_fields(Some(&s), &f(&[("tdm-reservation", " 1\t")])), Ok(vec![])); + assert_eq!(check_fields(Some(&s), &f(&[("tdm-reservation", "0")])), Err(Reason::HeaderConflicts)); + assert_eq!(check_fields(Some(&s), &f(&[("tdm-policy", "https://p.example/x")])), Err(Reason::HeaderConflicts)); + assert_eq!(check_fields(None, &f(&[("Content-Usage", "train-ai=y")])), Ok(vec![Vocabulary::Aipref])); + } + + #[test] + fn codes_are_unique_and_named() { + let set: BTreeSet<&str> = CODES.iter().copied().collect(); + assert_eq!(set.len(), CODES.len()); + for r in [ + claim_receipt::Reason::ConsumptionObjectsUnordered, + claim_receipt::Reason::ConsumptionSubjectMismatch, + claim_receipt::Reason::ConsumptionUsageUnordered, + claim_receipt::Reason::ConsumptionTermUnknown, + claim_receipt::Reason::ConsumptionSeenUnordered, + claim_receipt::Reason::ConsumptionSignerNotPrincipal, + ] { + assert!(set.contains(Reason::Claim(r).code())); + } + } +} diff --git a/wai/wai-rs/src/ai_usage/corpus.rs b/wai/wai-rs/src/ai_usage/corpus.rs new file mode 100644 index 000000000..0262f5a24 --- /dev/null +++ b/wai/wai-rs/src/ai_usage/corpus.rs @@ -0,0 +1,1184 @@ +//! The conformance corpus of extensions/ai-usage.md §10: `live-conformance/usage/`. +//! +//! [`generate`] writes `vectors.json` from fixed seeds: every key is SHA-256 of this corpus's +//! label and an index, every time is a constant, every statement and payload is written here, +//! and Ed25519 signing is deterministic (RFC 8032), so the file is the same bytes on every +//! machine. Each case states the outcome it was written to have; the generator asserts that +//! this module reaches it before writing it, and that the corpus gives every reason of §9. +//! +//! [`verify`] checks a corpus directory's files against this module. +//! `live-conformance/ai_usage_verify.py` checks the same file from the extension's text, +//! sharing no code with this module. + +use super::*; +use crate::claim_receipt::{ClaimBody, SubjectType}; +use crate::jwp_delivery::{resign, GroupBuilder}; +use crate::live_events::Policy as EventPolicy; +use crate::live_room::{Caps, Limits, Recording, Review, Role, NO_CEILING}; +use ed25519_dalek::SigningKey; +use serde_json::json; + +const T0_US: u64 = 1_792_540_800_000_000; +const T0_S: u64 = T0_US / 1_000_000; +const S: u64 = 1_000_000; +const ISSUED: &str = "2026-10-21T12:00:00Z"; +const GROUP_START: &str = "2026-10-21T09:00:00Z"; +const GROUP_END: &str = "2026-10-21T09:00:10Z"; + +const PUBLISHER: u8 = 0; +const AUTHORITY: u8 = 1; +const CLOCK: u8 = 2; +const PRESENTER: u8 = 3; +const AGENT_A: u8 = 4; +const AGENT_B: u8 = 5; +const CRAWLER: u8 = 6; +const AGENT_D: u8 = 7; +const STRANGER: u8 = 8; +const KEYS: [(&str, u8); 9] = [ + ("publisher", PUBLISHER), + ("authority", AUTHORITY), + ("clock", CLOCK), + ("presenter", PRESENTER), + ("agent_a", AGENT_A), + ("agent_b", AGENT_B), + ("crawler", CRAWLER), + ("agent_d", AGENT_D), + ("stranger", STRANGER), +]; + +fn key(i: u8) -> SigningKey { + SigningKey::from_bytes(&sha256(&[b"wai:ai-usage/corpus/key\x00", &[i]])) +} + +fn pk(i: u8) -> [u8; 32] { + key(i).verifying_key().to_bytes() +} + +fn did(i: u8) -> String { + crate::signer_id::did_key(&pk(i)) +} + +// --------------------------------------------------------------------------------------- +// Statements +// --------------------------------------------------------------------------------------- + +const POLICY_URL: &str = "https://licensing.example/tdm-policy.json"; + +fn aipref(value: &str) -> Value { + json!({"revision": AIPREF_REVISION, "value": value}) +} + +fn tdmrep(reservation: u64, policy: Option<&str>) -> Value { + json!({"revision": TDMREP_REVISION, "tdm-reservation": reservation, "tdm-policy": policy}) +} + +/// The catalogue's statement: no training, inference and search allowed, mining reserved. +fn s_catalogue() -> Value { + json!({ + "aipref": aipref("train-ai=n, ai-use=y, search=y"), + "cawg": {"revision": CAWG_REVISION, "entries": { + "cawg.ai_generative_training": {"use": "notAllowed"}, + "cawg.ai_inference": {"use": "allowed"}, + "cawg.data_mining": {"use": "constrained", "constraint_info": "licensed on request: https://licensing.example/tdm"} + }}, + "tdmrep": tdmrep(1, Some(POLICY_URL)) + }) +} + +/// A statement that says only that training is not allowed. +fn s_no_training() -> Value { + json!({"aipref": aipref("train-ai=n")}) +} + +/// The lecture room's first record: inference allowed, training and search not, mining +/// reserved. +fn s_room_first() -> Value { + json!({"aipref": aipref("train-ai=n, ai-use=y, search=n"), "tdmrep": tdmrep(1, None)}) +} + +/// The lecture room's second record, a minute in: inference no longer allowed. +fn s_room_second() -> Value { + json!({"aipref": aipref("train-ai=n, ai-use=n, search=n"), "tdmrep": tdmrep(1, None)}) +} + +/// One presenter object's own statement: search allowed. +fn s_search() -> Value { + json!({"aipref": aipref("search=y")}) +} + +/// Agent A's terms: inference. +fn t_inference() -> Value { + json!({"aipref": aipref("ai-use=y")}) +} + +/// Agent B's terms: a licence to train and mine. +fn t_licence() -> Value { + json!({"aipref": aipref("train-ai=y, ai-use=y"), "tdmrep": tdmrep(0, None)}) +} + +fn canon(v: &Value) -> String { + crate::jcs::jcs(v).expect("a statement has a canonical form") +} + +fn stmt(v: &Value) -> Statement { + Statement::read(v).expect("the corpus's statements read") +} + +/// The statement cases: name, description, JSON text, and the outcome each is written to have +/// (`None` to be accepted). +fn statement_cases() -> Vec { + let ok = |name: &str, about: &str, text: String| json!({"name": name, "description": about, "json": text, "expect": "accept"}); + let bad = |name: &str, about: &str, text: String, reason: Reason| { + json!({"name": name, "description": about, "json": text, "expect": reason.code()}) + }; + let s = |v: Value| serde_json::to_string(&v).expect("json"); + let ap = |value: &str| s(json!({"aipref": aipref(value)})); + let long_entries: Map = (0..32) + .map(|i| (format!("org.example.use{i:02}"), json!({"use": "constrained", "constraint_info": "x".repeat(120)}))) + .collect(); + vec![ + ok("aipref_only", "one vocabulary: training disallowed, search allowed, inference unknown", ap("train-ai=n, search=y")), + ok("three_vocabularies", "the three vocabularies together, in a member order the canonical form sorts", s(s_catalogue())), + ok("cawg_custom_key", "a key the assertion lets a claim generator add, and a predefined one", s(json!({"cawg": {"revision": CAWG_REVISION, "entries": {"org.example.captioning": {"use": "allowed"}, "cawg.ai_training": {"use": "notAllowed"}}}}))), + ok("tdmrep_not_reserved", "TDM rights not reserved, and no policy", s(json!({"tdmrep": tdmrep(0, None)}))), + ok("aipref_repeated_key", "a repeated key keeps its last value (RFC 9651 §4.2.2)", ap("train-ai=n, train-ai=y")), + ok("aipref_parameters", "parameters are disregarded", ap("train-ai=y;source=\"publisher\", ai-use=n;q=0.5")), + ok("aipref_not_tokens", "a String, a Boolean and an inner list are not the Token y or n: unknown", ap("train-ai=\"y\", ai-use=?1, search=(y n)")), + ok("aipref_unknown_keys", "keys that are not labels are ignored; a key with no value is Boolean true", ap("foo=y, bar, *x=n, search=n")), + ok("aipref_every_bare_item", "every bare item type parses", ap("a=1, b=-2.5, c=\"s\\\"q\", d=tok/en:x, e=:aGk=:, f=?0, g=@1700000000, h=%\"caf%c3%a9\", i=(1 \"x\");p, train-ai=y")), + bad("empty_object", "a statement with no member", "{}".into(), Reason::UsageMalformed), + bad("unknown_member", "a member §3.1 does not list", s(json!({"aipref": aipref("train-ai=n"), "robots": {}})), Reason::UsageMalformed), + bad("aipref_double_comma", "a value that does not parse as a Dictionary", ap("train-ai=y,, search=n"), Reason::UsageMalformed), + bad("aipref_upper_case", "upper case fails the Dictionary (draft-ietf-aipref-vocab-08 §6.5.1)", ap("Train-AI=y"), Reason::UsageMalformed), + bad("aipref_trailing_comma", "a trailing comma", ap("train-ai=y,"), Reason::UsageMalformed), + bad("aipref_not_ascii", "a value that is not ASCII", ap("train-ai=\u{e9}"), Reason::UsageMalformed), + bad("aipref_empty_value", "an empty value", ap(""), Reason::UsageMalformed), + bad("tdmrep_reservation_two", "a reservation other than 0 or 1", s(json!({"tdmrep": tdmrep(2, None)})), Reason::UsageMalformed), + bad("tdmrep_policy_not_http", "a policy that is not an http or https URL", s(json!({"tdmrep": {"revision": TDMREP_REVISION, "tdm-reservation": 1, "tdm-policy": "ftp://licensing.example/p"}})), Reason::UsageMalformed), + bad("tdmrep_policy_absent", "a vocabulary object missing a member", s(json!({"tdmrep": {"revision": TDMREP_REVISION, "tdm-reservation": 1}})), Reason::UsageMalformed), + bad("cawg_reserved_prefix", "a cawg. key the assertion does not define", s(json!({"cawg": {"revision": CAWG_REVISION, "entries": {"cawg.ai_summaries": {"use": "allowed"}}}})), Reason::UsageMalformed), + bad("cawg_use_unknown", "a use the assertion does not define", s(json!({"cawg": {"revision": CAWG_REVISION, "entries": {"org.example.x": {"use": "maybe"}}}})), Reason::UsageMalformed), + bad("repeated_member", "a member named twice in one object", "{\"aipref\":{\"revision\":\"draft-ietf-aipref-vocab-08\",\"revision\":\"draft-ietf-aipref-vocab-08\",\"value\":\"train-ai=n\"}}".into(), Reason::UsageMalformed), + bad("canonical_too_long", "a canonical form longer than 4096 bytes", s(json!({"cawg": {"revision": CAWG_REVISION, "entries": long_entries}})), Reason::UsageMalformed), + bad("aipref_revision_unknown", "a revision this extension does not read", s(json!({"aipref": {"revision": "draft-ietf-aipref-vocab-07", "value": "train-ai=n"}})), Reason::UsageRevisionUnknown), + bad("cawg_revision_unknown", "the assertion's earlier version", s(json!({"cawg": {"revision": "1.0", "entries": {"cawg.ai_training": {"use": "allowed"}}}})), Reason::UsageRevisionUnknown), + bad("malformed_before_revision", "a malformed vocabulary is refused before an unknown revision", s(json!({"aipref": {"revision": "draft-ietf-aipref-vocab-07", "value": "train-ai=n"}, "tdmrep": tdmrep(2, None)})), Reason::UsageMalformed), + ] +} + +// --------------------------------------------------------------------------------------- +// Envelopes and publications +// --------------------------------------------------------------------------------------- + +/// A `WAI1` envelope: the manifest, written in a fixed member order with no insignificant +/// whitespace, and an opaque payload no case decodes. +fn wai1(statement: Option<&Value>, live_t: Option, payload: &[u8]) -> Vec { + let mut m = String::from( + r#"{"wai":"1.0","media":"video","intent":"replicate","model_requirement":{"capability":"wai.video.av1","fallback":null}"#, + ); + if let Some(t) = live_t { + m.push_str(&format!(r#","live":{{"t":{t},"rtp":{},"rate":90000,"key":true}}"#, t * 90 / 1000)); + } + if let Some(s) = statement { + m.push_str(&format!(r#","ai_usage":{}"#, canon(s))); + } + m.push('}'); + let mut o = b"WAI1".to_vec(); + o.extend_from_slice(&(m.len() as u32).to_le_bytes()); + o.extend_from_slice(m.as_bytes()); + o.extend_from_slice(&(payload.len() as u32).to_le_bytes()); + o.extend_from_slice(payload); + o +} + +/// A `WAI2` envelope of two renditions under one statement. +fn wai2(statement: &Value) -> Vec { + let p1 = b"rendition one: opaque test bytes".to_vec(); + let p2 = b"rendition two: opaque test bytes, longer".to_vec(); + let m = format!( + r#"{{"wai":"1.1","media":"image","intent":"replicate","renditions":[{{"capability":"wai.image.avif","kind":"avif"}},{{"capability":"wai.image.jpeg","kind":"jpeg"}}],"ai_usage":{}}}"#, + canon(statement) + ); + let mut o = b"WAI2".to_vec(); + o.extend_from_slice(&(m.len() as u32).to_le_bytes()); + o.extend_from_slice(m.as_bytes()); + o.extend_from_slice(&2u16.to_le_bytes()); + o.extend_from_slice(&0u32.to_le_bytes()); + o.extend_from_slice(&(p1.len() as u32).to_le_bytes()); + o.extend_from_slice(&(p1.len() as u32).to_le_bytes()); + o.extend_from_slice(&(p2.len() as u32).to_le_bytes()); + o.extend_from_slice(&p1); + o.extend_from_slice(&p2); + o +} + +/// A publication: one group of objects under one key. +struct Publication { + name: &'static str, + room: Option<&'static str>, + signer: u8, + group: GroupReceipt, + /// (name, envelope, receipt). + objects: Vec<(&'static str, Vec, ObjectReceipt)>, +} + +fn publish(name: &'static str, room: Option<&'static str>, signer: u8, track: u64, objects: Vec<(&'static str, Vec)>) -> Publication { + let mut b = GroupBuilder::default(); + for (_, e) in &objects { + b.add(crate::jwp_hash::content_hash(e), 0, "", None); + } + let id = did(signer); + let (group, receipts) = + b.finish(&key(signer), &id, track, 1, GROUP_START, GROUP_END, None, false).expect("unmetered objects compose"); + let objects = objects.into_iter().zip(receipts).map(|((n, e), r)| (n, e, r)).collect(); + Publication { name, room, signer, group, objects } +} + +struct Corpus { + publications: Vec, + /// Envelopes presented with another object's receipt: (name, publication, object, envelope). + impostors: Vec<(&'static str, &'static str, &'static str, Vec, &'static str)>, + rooms: Vec, +} + +impl Corpus { + fn publication(&self, name: &str) -> &Publication { + self.publications.iter().find(|p| p.name == name).expect("a publication of the corpus") + } + + fn envelope(&self, p: &str, o: &str) -> &[u8] { + &self.publication(p).objects.iter().find(|x| x.0 == o).expect("an object of the publication").1 + } + + fn hash(&self, p: &str, o: &str) -> [u8; 32] { + crate::jwp_hash::content_hash(self.envelope(p, o)) + } + + fn room(&self, name: &str) -> &RoomCase { + self.rooms.iter().find(|r| r.name == name).expect("a room of the corpus") + } + + /// The objects a claim's context holds, by content hash. + fn held(&self, pubs: &[&str]) -> BTreeMap<[u8; 32], (HeldObject, bool)> { + let mut m = BTreeMap::new(); + for p in pubs { + let p = self.publication(p); + for (_, e, r) in &p.objects { + if let Ok(h) = hold_object(e, r, &p.group) { + m.insert(h.content_hash, (h, p.room.is_some())); + } + } + } + m + } +} + +// --------------------------------------------------------------------------------------- +// Rooms +// --------------------------------------------------------------------------------------- + +struct RoomCase { + name: &'static str, + description: &'static str, + descriptor: Vec, + grants: Vec>, + records: Vec>, +} + +impl RoomCase { + fn checked(&self) -> CheckedRoom { + check_room(&self.descriptor, &self.grants, &self.records).expect("the corpus's descriptors verify") + } +} + +fn descriptor() -> Descriptor { + let caps = |publish: u8| Caps { publish, subscribe: true, max_layer: 1, powers: 0 }; + Descriptor { + authority: pk(AUTHORITY), + clock: pk(CLOCK), + t0_us: T0_US, + tick_us: S, + origin: String::new(), + limits: Limits { max_present: 16, max_publishers: 2, reval_s: 0, ceiling: EventPolicy::default() }, + roles: vec![ + Role { name: "presenter".into(), caps: caps(0b010), events: [0; 32] }, + Role { name: "agent".into(), caps: caps(0), events: [0; 32] }, + ], + slots: vec![], + profiles: vec![], + review: Review { kinds: [0; 32], deadline_ms: 0, fallback: 0 }, + recording: Recording { mode: 0, retention_days: 0, commit_interval_ms: 0, jitter_ms: 0, regions: vec![] }, + sig: [0; 64], + } + .sign(&key(AUTHORITY)) +} + +fn grant(d: &Descriptor, who: u8, role: u8, attrs: Vec<(String, String)>) -> Grant { + Grant { + room: d.room_id(), + principal: pk(who), + prev: [0; 32], + effective: 0, + nbf: T0_S - 3_600, + exp: T0_S + 86_400, + role, + caps: d.roles[role as usize].caps, + precedence: 0, + once: [0; 16], + viewer: [0; 16], + policy: EventPolicy::with_kinds(&[]), + joules_ceiling: NO_CEILING, + funds_ceiling: NO_CEILING, + currency: [0; 3], + profiles: vec![], + origins: vec!["https://relay.rooms.example".into()], + attrs, + sig: [0; 64], + } + .sign(&key(AUTHORITY)) +} + +fn terms_attr(v: &Value) -> Vec<(String, String)> { + vec![(GRANT_ATTRIBUTE.into(), canon(v))] +} + +fn record(room: [u8; 32], prev: [u8; 32], effective: u64, v: &Value, signer: u8) -> RoomRecord { + RoomRecord { room, prev, effective, statement: stmt(v), sig: [0; 64] }.sign(&key(signer)) +} + +/// A record's bytes with its statement replaced by `statement`, signed again. +fn record_with_bytes(room: [u8; 32], prev: [u8; 32], effective: u64, statement: &[u8], signer: u8) -> Vec { + use ed25519_dalek::Signer; + let mut o = ROOM_MAGIC.to_vec(); + o.push(RECORD_VERSION); + o.extend_from_slice(&room); + o.extend_from_slice(&prev); + o.extend_from_slice(&effective.to_le_bytes()); + o.extend_from_slice(&(statement.len() as u16).to_le_bytes()); + o.extend_from_slice(statement); + let mut m = ROOM_DOMAIN.to_vec(); + m.extend_from_slice(&o); + let sig = key(signer).sign(&m).to_bytes(); + o.extend_from_slice(&sig); + o +} + +fn rooms() -> Vec { + let d = descriptor(); + let id = d.room_id(); + let presenter = grant(&d, PRESENTER, 0, vec![]); + let agent_a = grant(&d, AGENT_A, 1, terms_attr(&t_inference())); + let agent_b = grant(&d, AGENT_B, 1, terms_attr(&t_licence())); + let grants: Vec> = [&presenter, &agent_a, &agent_b].iter().map(|g| g.to_bytes()).collect(); + let r0 = record(id, [0; 32], 0, &s_room_first(), AUTHORITY); + let r1 = record(id, r0.digest(), 60 * S, &s_room_second(), AUTHORITY); + let records = vec![r0.to_bytes(), r1.to_bytes()]; + let other_room = sha256(&[b"wai:ai-usage/corpus/another room"]); + let room = |name, description, grants: Vec>, records: Vec>| RoomCase { + name, + description, + descriptor: d.to_bytes(), + grants, + records, + }; + let mut truncated = r0.to_bytes(); + truncated.truncate(truncated.len() - 1); + let mut version = r0.to_bytes(); + version[4] = 2; + let mut magic = r0.to_bytes(); + magic[3] = b'2'; + let spaced = format!(" {}", canon(&s_room_first())); + let bad_terms = grant(&d, AGENT_A, 1, vec![(GRANT_ATTRIBUTE.into(), "{\"aipref\": \"ai-use=y\"}".into())]); + vec![ + room("lecture", "a room with two chained usage records and two agent principals, one of whose terms lift a disallow", grants.clone(), records.clone()), + room("record_of_another_room", "a record naming another room", grants.clone(), vec![record(other_room, [0; 32], 0, &s_room_first(), AUTHORITY).to_bytes()]), + room("record_not_the_authoritys", "a record signed by a key other than the authority", grants.clone(), vec![record(id, [0; 32], 0, &s_room_first(), STRANGER).to_bytes()]), + room("record_unlinked", "a second record that does not name the first", grants.clone(), vec![r0.to_bytes(), record(id, [0; 32], 60 * S, &s_room_second(), AUTHORITY).to_bytes()]), + room("record_not_later", "a second record that takes effect no later than the first", grants.clone(), vec![r0.to_bytes(), record(id, r0.digest(), 0, &s_room_second(), AUTHORITY).to_bytes()]), + room("record_not_canonical", "a record whose statement bytes are not the canonical form", grants.clone(), vec![record_with_bytes(id, [0; 32], 0, spaced.as_bytes(), AUTHORITY)]), + room("record_statement_malformed", "a record whose statement does not read", grants.clone(), vec![record_with_bytes(id, [0; 32], 0, canon(&json!({"tdmrep": tdmrep(2, None)})).as_bytes(), AUTHORITY)]), + room("record_truncated", "a record one byte short", grants.clone(), vec![truncated]), + room("record_unknown_version", "a record of version 2", grants.clone(), vec![version]), + room("record_wrong_magic", "a record whose magic is not WAU1", grants.clone(), vec![magic]), + room("grant_terms_malformed", "a grant whose terms do not read: the grant is held, with no terms", vec![presenter.to_bytes(), bad_terms.to_bytes()], records), + ] +} + + +fn room_inputs(r: &RoomCase, expect: &str) -> Value { + json!({ + "name": r.name, + "description": r.description, + "descriptor": hex(&r.descriptor), + "grants": r.grants.iter().map(|g| hex(g)).collect::>(), + "records": r.records.iter().map(|g| hex(g)).collect::>(), + "expect": expect, + }) +} + +// --------------------------------------------------------------------------------------- +// The corpus's objects +// --------------------------------------------------------------------------------------- + +fn corpus() -> Corpus { + let catalogue = publish( + "catalogue", + None, + PUBLISHER, + 1, + vec![ + ("feature", wai1(Some(&s_catalogue()), None, b"feature: opaque test bytes")), + ("stills", wai2(&s_catalogue())), + ("trailer", wai1(Some(&s_no_training()), None, b"trailer: opaque test bytes")), + ("credits", wai1(None, None, b"credits: opaque test bytes")), + ], + ); + let unreadable = publish( + "unreadable", + None, + PUBLISHER, + 2, + vec![("reserved_twice", wai1(Some(&json!({"tdmrep": tdmrep(2, None)})), None, b"opaque"))], + ); + let mut forged = publish("forged", None, PUBLISHER, 3, vec![("signed_by_another", wai1(Some(&s_catalogue()), None, b"opaque"))]); + forged.group = resign(forged.group, &key(STRANGER)); + let lecture = publish( + "lecture_media", + Some("lecture"), + PRESENTER, + 4, + vec![ + ("at_10s", wai1(None, Some(10 * S), b"lecture at 10 s")), + ("at_20s", wai1(Some(&s_search()), Some(20 * S), b"lecture at 20 s")), + ("at_70s", wai1(None, Some(70 * S), b"lecture at 70 s")), + ("untimed", wai1(None, None, b"lecture, untimed")), + ], + ); + let mut altered = s_catalogue(); + altered["aipref"]["value"] = "train-ai=y, ai-use=y, search=y".into(); + let impostors = vec![ + ("feature_stripped", "catalogue", "feature", wai1(None, None, b"feature: opaque test bytes"), "the feature with its statement removed, under the feature's receipt"), + ("feature_altered", "catalogue", "feature", wai1(Some(&altered), None, b"feature: opaque test bytes"), "the feature with training allowed in its statement, under the feature's receipt"), + ]; + Corpus { publications: vec![catalogue, unreadable, forged, lecture], impostors, rooms: rooms() } +} + +fn object_inputs(c: &Corpus) -> Vec { + let mut out = Vec::new(); + for p in &c.publications { + for (n, e, r) in &p.objects { + let expect = match (p.name, *n) { + ("unreadable", _) => "usage_malformed", + ("forged", _) => "receipt_invalid", + _ => "held", + }; + out.push(json!({"name": n, "description": "an object of its publication, under its own receipt", "publication": p.name, + "envelope": hex(e), "receipt": r.to_json(), "expect": expect})); + } + } + for (name, p, o, e, about) in &c.impostors { + let r = &c.publication(p).objects.iter().find(|x| x.0 == *o).expect("the object").2; + out.push(json!({"name": name, "description": about, "publication": p, "envelope": hex(e), "receipt": r.to_json(), + "expect": "usage_unbound"})); + } + out +} + +fn response_inputs() -> Vec { + let case = |name: &str, about: &str, object: &str, fields: &[(&str, &str)], expect: &str| { + json!({"name": name, "description": about, "publication": "catalogue", "object": object, + "fields": fields.iter().map(|(a, b)| json!([a, b])).collect::>(), "expect": expect}) + }; + vec![ + case("respelled", "the fields in another spelling: other order, no space, a parameter, an unknown key, other case", "feature", + &[("content-usage", "search=y,ai-use=y;x=1, train-ai=n, foo=y"), ("TDM-Reservation", " 1"), ("tdm-policy", POLICY_URL)], "accept"), + case("split_lines", "Content-Usage on two field lines, combined", "feature", + &[("Content-Usage", "train-ai=n"), ("Content-Usage", "ai-use=y, search=y")], "accept"), + case("aipref_contradicts", "Content-Usage allowing training the signed statement disallows", "feature", + &[("Content-Usage", "train-ai=y, ai-use=y, search=y")], "header_conflicts"), + case("aipref_drops_a_label", "Content-Usage that leaves search unknown", "feature", &[("Content-Usage", "train-ai=n, ai-use=y")], "header_conflicts"), + case("aipref_unparseable", "Content-Usage that does not parse", "feature", &[("Content-Usage", "train-ai=n,, ai-use=y, search=y")], "header_conflicts"), + case("tdm_contradicts", "tdm-reservation 0 against a signed 1", "feature", &[("tdm-reservation", "0")], "header_conflicts"), + case("tdm_policy_contradicts", "a tdm-policy other than the signed one", "feature", + &[("tdm-reservation", "1"), ("tdm-policy", "https://elsewhere.example/p.json")], "header_conflicts"), + case("tdm_twice", "tdm-reservation on two field lines", "feature", &[("tdm-reservation", "1"), ("tdm-reservation", "1")], "header_conflicts"), + case("unsigned_only", "fields for an object with no signed statement: unsigned statements, no conflict", "credits", + &[("Content-Usage", "train-ai=n"), ("tdm-reservation", "1")], "accept"), + case("unsigned_vocabulary", "a TDMRep field for an object whose statement carries aipref alone", "trailer", + &[("Content-Usage", "train-ai=n"), ("tdm-reservation", "1")], "accept"), + ] +} + +fn catalog_inputs() -> Vec { + let entry = |usage: Option<&str>| -> String { + match usage { + None => r#"{"name":"feature","packaging":"wai","wai_capabilities":["wai.video.av1"]}"#.into(), + Some(u) => format!(r#"{{"name":"feature","packaging":"wai","wai_capabilities":["wai.video.av1"],"wai_ai_usage":{u}}}"#), + } + }; + // The catalogue's statement written with its members in another order and with spaces. + let respelled = r#"{ "tdmrep": {"tdm-policy": "https://licensing.example/tdm-policy.json", "tdm-reservation": 1, "revision": "CG-FINAL-tdmrep-20240510"}, "cawg": {"entries": {"cawg.data_mining": {"constraint_info": "licensed on request: https://licensing.example/tdm", "use": "constrained"}, "cawg.ai_inference": {"use": "allowed"}, "cawg.ai_generative_training": {"use": "notAllowed"}}, "revision": "1.1"}, "aipref": {"value": "train-ai=n, ai-use=y, search=y", "revision": "draft-ietf-aipref-vocab-08"} }"#; + let other = canon(&json!({"aipref": aipref("train-ai=y")})); + let case = |name: &str, about: &str, objects: &[&str], text: String, expect: &str| { + json!({"name": name, "description": about, "publication": "catalogue", "objects": objects, "entry": text, "expect": expect}) + }; + vec![ + case("agrees", "the catalog states the objects' statement, in another spelling", &["feature", "stills"], entry(Some(respelled)), "accept"), + case("states_nothing", "a catalog that states no preference", &["feature", "stills"], entry(None), "accept"), + case("objects_state_nothing", "a catalog stating a preference over objects that carry none", &["credits"], entry(Some(&other)), "accept"), + case("contradicts", "a catalog allowing training the objects' statement disallows", &["feature", "stills"], entry(Some(&other)), "catalog_conflicts"), + case("does_not_read", "a catalog whose statement does not read", &["feature"], entry(Some("{}")), "catalog_conflicts"), + ] +} + +// --------------------------------------------------------------------------------------- +// Claims and gates +// --------------------------------------------------------------------------------------- + +/// What a claim is written from, and the outcome it is written to have. +struct ClaimSpec { + name: &'static str, + description: &'static str, + signer: u8, + signer_id: Option, + objects: Vec<[u8; 32]>, + subject: Option<[u8; 32]>, + usage: Vec<(Vocabulary, Option<&'static str>)>, + seen: Vec<[u8; 32]>, + grant: Option<[u8; 32]>, + seq: u64, + /// The claim before it of its signer, which its parent names. + previous: Option<&'static str>, + /// Whether the verifier is given that claim. + give_previous: bool, + publications: Vec<&'static str>, + room: Option<&'static str>, + /// Flip a bit of the signature. + bad_sig: bool, + /// Keep the order given rather than sorting (the class-rule controls). + keep_order: bool, + /// The wire with its first `"vocabulary":"aipref"` naming this instead (a step 2 control). + vocabulary_label: Option<&'static str>, + expect: &'static str, +} + +impl ClaimSpec { + fn new(name: &'static str, description: &'static str, signer: u8, expect: &'static str) -> ClaimSpec { + ClaimSpec { + name, + description, + signer, + signer_id: None, + objects: vec![], + subject: None, + usage: vec![], + seen: vec![], + grant: None, + seq: 0, + previous: None, + give_previous: true, + publications: vec!["catalogue"], + room: None, + bad_sig: false, + keep_order: false, + vocabulary_label: None, + expect, + } + } +} + +/// The digests of the governing statements of `objects`, ascending. +fn seen_of(objects: &[[u8; 32]], held: &BTreeMap<[u8; 32], (HeldObject, bool)>, room: Option<&CheckedRoom>) -> Vec<[u8; 32]> { + let set: BTreeSet<[u8; 32]> = objects + .iter() + .filter_map(|h| held.get(h)) + .flat_map(|(o, r)| governing(o, if *r { room } else { None }).into_iter().map(Statement::digest)) + .collect(); + set.into_iter().collect() +} + +fn claim_specs(c: &Corpus) -> Vec { + use Vocabulary::*; + let cat = |o: &str| c.hash("catalogue", o); + let lec = |o: &str| c.hash("lecture_media", o); + let lecture = c.room("lecture").checked(); + let held_cat = c.held(&["catalogue"]); + let held_lec = c.held(&["lecture_media"]); + let seen_cat = |os: &[[u8; 32]]| seen_of(os, &held_cat, None); + let seen_lec = |os: &[[u8; 32]]| seen_of(os, &held_lec, Some(&lecture)); + let grant_of = |who: u8| -> [u8; 32] { + lecture.grants.iter().find(|(g, _)| g.grant.principal == pk(who)).expect("the agent's grant").0.digest + }; + let all_three = || vec![(Aipref, Some("ai-use")), (Cawg, Some("cawg.ai_inference")), (Tdmrep, None)]; + let mut v = Vec::new(); + + // The crawler's chain: inference allowed, then an unknown use, then a forbidden one. + let mut s = ClaimSpec::new("inference_allowed", "the crawler used the feature and the stills for inference, which the statement allows", CRAWLER, "accept"); + s.objects = vec![cat("feature"), cat("stills")]; + s.usage = all_three(); + s.seen = seen_cat(&s.objects); + v.push(s); + let mut s = ClaimSpec::new("unknown_use", "the crawler's second claim: inference over the trailer, whose statement says nothing of inference", CRAWLER, "accept"); + s.objects = vec![cat("trailer")]; + s.usage = vec![(Aipref, Some("ai-use"))]; + s.seen = seen_cat(&s.objects); + s.seq = 1; + s.previous = Some("inference_allowed"); + v.push(s); + let mut s = ClaimSpec::new("training_forbidden", "the crawler's third claim: generative training on the feature, which the statement disallows in every vocabulary", CRAWLER, "usage_disallowed"); + s.objects = vec![cat("feature")]; + s.usage = vec![(Aipref, Some("train-ai")), (Cawg, Some("cawg.ai_generative_training")), (Tdmrep, Some("tdm-reservation"))]; + s.seen = seen_cat(&s.objects); + s.seq = 2; + s.previous = Some("unknown_use"); + v.push(s); + let mut s = ClaimSpec::new("outside", "a use outside the vocabulary, over an object with no statement", AGENT_D, "accept"); + s.objects = vec![cat("credits")]; + s.usage = vec![(Aipref, None)]; + v.push(s); + let mut s = ClaimSpec::new("mining_constrained", "mining the feature: constrained in the assertion, which reads as not allowed without a grant", AGENT_D, "usage_disallowed"); + s.objects = vec![cat("feature")]; + s.usage = vec![(Aipref, Some("ai-use")), (Cawg, Some("cawg.data_mining")), (Tdmrep, None)]; + s.seen = seen_cat(&s.objects); + v.push(s); + + // Controls, each the first claim of its signer unless it says otherwise. + let mut s = ClaimSpec::new("wrong_digests", "a claim naming an object the verifier does not hold: the stripped feature's content hash", AGENT_D, "consumption_object_unknown"); + s.objects = vec![crate::jwp_hash::content_hash(&c.impostors[0].3)]; + s.usage = vec![(Aipref, Some("ai-use"))]; + v.push(s); + let mut s = ClaimSpec::new("bad_signature", "an accepted claim with one bit of its signature changed", AGENT_D, "signature_verify_failed"); + s.objects = vec![cat("feature"), cat("stills")]; + s.usage = all_three(); + s.seen = seen_cat(&s.objects); + s.bad_sig = true; + v.push(s); + let mut s = ClaimSpec::new("preference_mismatch", "a claim that says it saw a statement other than the one that governs", AGENT_D, "preference_mismatch"); + s.objects = vec![cat("feature")]; + s.usage = all_three(); + s.seen = vec![stmt(&s_no_training()).digest()]; + v.push(s); + let mut s = ClaimSpec::new("unanswered", "a claim answering aipref alone over an object whose statement carries all three vocabularies", AGENT_D, "usage_unanswered"); + s.objects = vec![cat("feature")]; + s.usage = vec![(Aipref, Some("ai-use"))]; + s.seen = seen_cat(&s.objects); + v.push(s); + let mut s = ClaimSpec::new("term_invented", "a term no vocabulary defines", AGENT_D, "consumption_term_unknown"); + s.objects = vec![cat("trailer")]; + s.usage = vec![(Aipref, Some("inference"))]; + s.seen = seen_cat(&s.objects); + v.push(s); + let mut s = ClaimSpec::new("cawg_term_reserved", "a cawg. key the assertion does not define", AGENT_D, "consumption_term_unknown"); + s.objects = vec![cat("feature")]; + s.usage = vec![(Aipref, Some("ai-use")), (Cawg, Some("cawg.ai_summaries")), (Tdmrep, None)]; + s.seen = seen_cat(&s.objects); + v.push(s); + let mut s = ClaimSpec::new("objects_unordered", "objects in descending order", AGENT_D, "consumption_objects_unordered"); + let mut os = vec![cat("feature"), cat("stills")]; + os.sort(); + os.reverse(); + s.seen = seen_cat(&os); + s.objects = os; + s.usage = all_three(); + s.keep_order = true; + v.push(s); + let mut s = ClaimSpec::new("subject_not_first", "a subject other than the first object", AGENT_D, "consumption_subject_mismatch"); + s.objects = vec![cat("feature"), cat("stills")]; + s.subject = s.objects.iter().max().copied(); + s.usage = all_three(); + s.seen = seen_cat(&s.objects); + v.push(s); + let mut s = ClaimSpec::new("usage_unordered", "usage entries out of vocabulary order", AGENT_D, "consumption_usage_unordered"); + s.objects = vec![cat("feature")]; + s.usage = vec![(Tdmrep, None), (Aipref, Some("ai-use")), (Cawg, Some("cawg.ai_inference"))]; + s.seen = seen_cat(&s.objects); + s.keep_order = true; + v.push(s); + let mut s = ClaimSpec::new("seen_unordered", "seen digests in descending order", AGENT_D, "consumption_seen_unordered"); + s.objects = vec![cat("feature"), cat("trailer")]; + s.usage = all_three(); + let mut seen = seen_cat(&s.objects); + seen.reverse(); + s.seen = seen; + s.keep_order = true; + s.objects.sort(); + v.push(s); + let mut s = ClaimSpec::new("signer_not_principal", "a signer_id that is not the did:key of the signing key", AGENT_D, "consumption_signer_not_principal"); + s.signer_id = Some(did(STRANGER)); + s.objects = vec![cat("credits")]; + s.usage = vec![(Aipref, None)]; + v.push(s); + let mut s = ClaimSpec::new("vocabulary_unknown", "a usage entry naming a vocabulary outside the three", AGENT_D, "field_type"); + s.objects = vec![cat("credits")]; + s.usage = vec![(Aipref, None)]; + s.vocabulary_label = Some("robots"); + v.push(s); + let mut s = ClaimSpec::new("parent_not_given", "a second claim whose first the verifier is not given", AGENT_D, "parent_not_held"); + s.objects = vec![cat("credits")]; + s.usage = vec![(Aipref, None)]; + s.seq = 1; + s.previous = Some("outside"); + s.give_previous = false; + v.push(s); + + // The lecture room. + let room_claim = |name, description, who: u8, objects: Vec<[u8; 32]>, usage: Vec<(Vocabulary, Option<&'static str>)>, grant, expect| { + let mut s = ClaimSpec::new(name, description, who, expect); + s.seen = seen_lec(&objects); + s.objects = objects; + s.usage = usage; + s.grant = grant; + s.publications = vec!["lecture_media"]; + s.room = Some("lecture"); + s + }; + let room_use = || vec![(Aipref, Some("ai-use")), (Tdmrep, None)]; + let training = || vec![(Aipref, Some("train-ai")), (Tdmrep, Some("tdm-reservation"))]; + v.push(room_claim("room_inference", "agent A, under its grant, used the lecture at 10 s for inference, which the room's first record allows", AGENT_A, + vec![lec("at_10s")], room_use(), Some(grant_of(AGENT_A)), "accept")); + let mut s = room_claim("room_terms_lift", "agent A used the lecture at 70 s for inference: the second record disallows it, and A's terms allow it", AGENT_A, + vec![lec("at_70s")], room_use(), Some(grant_of(AGENT_A)), "accept"); + s.seq = 1; + s.previous = Some("room_inference"); + v.push(s); + let mut s = room_claim("room_training_unlicensed", "agent A trained on the lecture at 10 s: its terms say nothing of training, and the room disallows it", AGENT_A, + vec![lec("at_10s")], training(), Some(grant_of(AGENT_A)), "usage_disallowed"); + s.seq = 2; + s.previous = Some("room_terms_lift"); + v.push(s); + let mut s = room_claim("room_untimed", "an object of the room with no live.t is governed by every record; A's terms allow inference", AGENT_A, + vec![lec("untimed")], room_use(), Some(grant_of(AGENT_A)), "accept"); + s.seq = 3; + s.previous = Some("room_training_unlicensed"); + v.push(s); + v.push(room_claim("room_licensed_training", "agent B trained on the lecture at 10 s under a licence that lifts the room's disallow in both vocabularies", AGENT_B, + vec![lec("at_10s")], training(), Some(grant_of(AGENT_B)), "accept")); + let mut s = room_claim("room_own_and_room", "search over an object whose own statement allows it and whose room disallows it: the most restrictive governs", AGENT_B, + vec![lec("at_20s")], vec![(Aipref, Some("search")), (Tdmrep, None)], Some(grant_of(AGENT_B)), "usage_disallowed"); + s.seq = 1; + s.previous = Some("room_licensed_training"); + v.push(s); + let mut s = room_claim("room_grant_on_another_object", "agent B naming its grant over an object that is not the room's", AGENT_B, + vec![cat("credits")], vec![(Aipref, None)], Some(grant_of(AGENT_B)), "grant_mismatch"); + s.publications = vec!["catalogue", "lecture_media"]; + s.seen = vec![]; + s.seq = 2; + s.previous = Some("room_own_and_room"); + v.push(s); + v.push(room_claim("room_grant_not_the_signers", "a key that is not agent B's naming agent B's grant", STRANGER, + vec![lec("at_10s")], room_use(), Some(grant_of(AGENT_B)), "grant_mismatch")); + v.push(room_claim("room_without_grant", "an agent with no grant using the room's object for inference: the room's first record governs", CRAWLER, + vec![lec("at_10s")], room_use(), None, "accept")); + v +} + +/// The claim a spec describes, signed, and its wire text. +fn write_claim(s: &ClaimSpec, previous: Option<&ClaimReceipt>) -> (String, ClaimReceipt) { + let mut objects = s.objects.clone(); + let mut usage = s.usage.clone(); + if !s.keep_order { + objects.sort(); + usage.sort_by_key(|u| u.0); + } + let subject = s.subject.unwrap_or(objects[0]); + let body = ClaimBody::Consumption(ConsumptionClaim { + objects, + usage: usage.into_iter().map(|(vocabulary, t)| UsageEntry { vocabulary, term: t.map(str::to_owned) }).collect(), + seen: s.seen.clone(), + grant: s.grant, + }); + let id = s.signer_id.clone().unwrap_or_else(|| did(s.signer)); + let parent = previous.map(ClaimReceipt::link_hash); + let mut r = ClaimReceipt::seal_unchecked(&key(s.signer), id, SubjectType::Object, subject, s.seq, ISSUED, body, parent) + .expect("a canonical form"); + if s.bad_sig { + r.sig[63] ^= 0x01; + } + let mut wire = r.to_json(); + if let Some(label) = s.vocabulary_label { + wire = wire.replacen("\"vocabulary\":\"aipref\"", &format!("\"vocabulary\":\"{label}\""), 1); + } + (wire, r) +} + +fn claim_inputs(c: &Corpus) -> Vec { + let mut written: BTreeMap<&str, ClaimReceipt> = BTreeMap::new(); + let mut out = Vec::new(); + for s in claim_specs(c) { + let previous = s.previous.map(|p| written.get(p).unwrap_or_else(|| panic!("{}: {p} is written first", s.name))); + let (wire, r) = write_claim(&s, previous); + out.push(json!({ + "name": s.name, + "description": s.description, + "wire": wire, + "context": {"publications": s.publications, "room": s.room, "previous": if s.give_previous { s.previous } else { None }}, + "expect": s.expect, + })); + written.insert(s.name, r); + } + out +} + +fn gate_inputs(c: &Corpus) -> Vec { + let cat = |o: &str| hex(&c.hash("catalogue", o)); + let lec = |o: &str| hex(&c.hash("lecture_media", o)); + let case = |name: &str, about: &str, window: u8, require_allowed: bool, agent: &str, served: Vec>, claims: &[&str], request: Vec, expect: &str| { + json!({"name": name, "description": about, "policy": {"window": window, "require_allowed": require_allowed}, "agent": agent, + "served": served, "claims": claims, "request": request, "expect": expect}) + }; + vec![ + case("claim_ahead_served", "the crawler claimed the feature before requesting it", 0, false, "crawler", vec![], &["inference_allowed"], vec![cat("feature")], "serve"), + case("claim_ahead_unclaimed", "a request for an object no claim names", 0, false, "crawler", vec![], &["inference_allowed"], vec![cat("credits")], "claim_required"), + case("claim_ahead_disallowed", "a request for an object the crawler also claimed for a use the statement disallows", 0, false, "crawler", vec![], + &["inference_allowed", "training_forbidden"], vec![cat("feature")], "usage_disallowed"), + case("unknown_served", "an unknown preference is served when the policy does not require allowed", 0, false, "crawler", vec![], &["unknown_use"], vec![cat("trailer")], "serve"), + case("unknown_not_granted", "an unknown preference under a policy that requires allowed", 0, true, "crawler", vec![], &["inference_allowed", "unknown_use"], vec![cat("trailer")], "usage_not_granted"), + case("claim_behind_within_window", "window 1: the last group served may be unclaimed", 1, false, "crawler", vec![vec![cat("feature")], vec![cat("trailer")]], + &["inference_allowed"], vec![cat("credits")], "serve"), + case("claim_behind_past_window", "window 1: an earlier group served and never claimed", 1, false, "crawler", + vec![vec![cat("feature")], vec![cat("trailer")], vec![cat("credits")]], &["inference_allowed"], vec![cat("stills")], "claim_required"), + case("another_agents_claim", "a claim signed by another agent is not held", 0, false, "agent_a", vec![], &["inference_allowed"], vec![cat("feature")], "claim_required"), + case("refused_claim_not_held", "a claim refused for another reason than the use is not held", 0, false, "agent_d", vec![], &["bad_signature"], vec![cat("feature")], "claim_required"), + case("room_agent_served", "agent A claimed the lecture at 10 s under its grant; the relay requires allowed", 0, true, "agent_a", vec![], &["room_inference"], vec![lec("at_10s")], "serve"), + case("room_agent_disallowed", "agent A's training claim over the lecture at 10 s", 0, true, "agent_a", vec![], + &["room_inference", "room_training_unlicensed"], vec![lec("at_10s")], "usage_disallowed"), + ] +} + +// --------------------------------------------------------------------------------------- +// Evaluation: the outcome of every case from the document's inputs +// --------------------------------------------------------------------------------------- + +fn unhex(s: &str) -> Option> { + claim_receipt::unhex(s) +} + +fn unhex32(s: &str) -> Option<[u8; 32]> { + unhex(s)?.try_into().ok() +} + +fn str_of<'a>(v: &'a Value, k: &str) -> Result<&'a str, String> { + v.get(k).and_then(Value::as_str).ok_or_else(|| format!("no string {k}")) +} + +fn bytes_of(v: &Value, k: &str) -> Result, String> { + unhex(str_of(v, k)?).ok_or_else(|| format!("{k} is not hex")) +} + +fn refusal(r: Reason) -> Value { + json!({"outcome": "refuse", "reason": r.code()}) +} + +fn statement_outcome(case: &Value) -> Result { + Ok(match Statement::read_text(str_of(case, "json")?) { + Ok(s) => { + let mut v = statement_report(&s); + v["outcome"] = "accept".into(); + v + } + Err(e) => refusal(e), + }) +} + +/// The document's publications' groups, by name. +fn groups(doc: &Value) -> Result)>, String> { + let mut m = BTreeMap::new(); + for p in doc["publications"].as_array().ok_or("no publications")? { + let g = GroupReceipt::from_json(&p["group"]).map_err(|e| format!("a group: {e:?}"))?; + m.insert(str_of(p, "name")?.to_owned(), (g, p["room"].as_str().map(str::to_owned))); + } + Ok(m) +} + +fn object_result(case: &Value, groups: &BTreeMap)>) -> Result, String> { + let (g, _) = groups.get(str_of(case, "publication")?).ok_or("an unknown publication")?; + let r = ObjectReceipt::from_json(&case["receipt"]).map_err(|e| format!("an object receipt: {e:?}"))?; + Ok(hold_object(&bytes_of(case, "envelope")?, &r, g)) +} + +fn object_outcome(case: &Value, groups: &BTreeMap)>) -> Result { + Ok(match object_result(case, groups)? { + Ok(h) => json!({"outcome": "held", "content_hash": hex(&h.content_hash), + "statement": h.statement.as_ref().map(|s| hex(&s.digest())), "t": h.t}), + Err(e) => refusal(e), + }) +} + +/// The held objects of each publication, by content hash, with whether each is a room's. +fn held_by_publication(doc: &Value) -> Result>, String> { + let gs = groups(doc)?; + let mut out: BTreeMap> = BTreeMap::new(); + for case in doc["objects"].as_array().ok_or("no objects")? { + let p = str_of(case, "publication")?; + if let Ok(h) = object_result(case, &gs)? { + let room = gs[p].1.is_some(); + out.entry(p.to_owned()).or_default().insert(h.content_hash, (h, room)); + } + } + Ok(out) +} + +fn room_of(doc: &Value, name: &str) -> Result { + let r = doc["rooms"].as_array().ok_or("no rooms")?.iter().find(|r| r["name"] == name).ok_or("an unknown room")?; + let list = |k: &str| -> Result>, String> { + r[k].as_array().ok_or("a list")?.iter().map(|x| x.as_str().and_then(unhex).ok_or_else(|| "hex".to_string())).collect() + }; + check_room(&bytes_of(r, "descriptor")?, &list("grants")?, &list("records")?).map_err(|e| format!("the descriptor: {}", e.code())) +} + +fn room_outcome(doc: &Value, case: &Value) -> Result { + let c = room_of(doc, str_of(case, "name")?)?; + let refused = |e: &(usize, Reason)| json!({"index": e.0, "reason": e.1.code()}); + let terms: Vec = c + .grants + .iter() + .map(|(h, t)| { + let terms = match t { + None => Value::Null, + Some(Ok(s)) => hex(&s.digest()).into(), + Some(Err(e)) => e.code().into(), + }; + json!({"grant": hex(&h.digest), "principal": hex(&h.grant.principal), "terms": terms}) + }) + .collect(); + let mut v = json!({ + "grants": c.grants_refused.as_ref().map_or(Value::from("accept"), refused), + "records": c.records.as_ref().map_or_else(refused, |_| Value::from("accept")), + "terms": terms, + }); + if c.records.is_ok() { + let at = |t: Option| -> Value { + json!({"t": t, "statements": c.records_at(t).iter().map(|r| hex(&r.statement.digest())).collect::>()}) + }; + v["in_effect"] = json!([at(Some(0)), at(Some(59 * S)), at(Some(60 * S)), at(Some(3_600 * S)), at(None)]); + } + Ok(v) +} + +fn response_outcome(case: &Value, held: &BTreeMap>, doc: &Value) -> Result { + let o = object_named(doc, str_of(case, "publication")?, str_of(case, "object")?, held)?; + let fields: Vec<(String, String)> = case["fields"] + .as_array() + .ok_or("fields")? + .iter() + .map(|f| Ok((f[0].as_str().ok_or("a name")?.to_owned(), f[1].as_str().ok_or("a value")?.to_owned()))) + .collect::>()?; + Ok(match check_fields(o.statement.as_ref(), &fields) { + Ok(u) => json!({"outcome": "accept", "unsigned": u.iter().map(|x| x.label()).collect::>()}), + Err(e) => refusal(e), + }) +} + +/// A held object of a publication by its case name. +fn object_named<'a>( + doc: &Value, + publication: &str, + name: &str, + held: &'a BTreeMap>, +) -> Result<&'a HeldObject, String> { + let case = doc["objects"] + .as_array() + .ok_or("no objects")? + .iter() + .find(|o| o["publication"] == publication && o["name"] == name) + .ok_or_else(|| format!("no object {publication}/{name}"))?; + let h = crate::jwp_hash::content_hash(&bytes_of(case, "envelope")?); + held.get(publication).and_then(|m| m.get(&h)).map(|x| &x.0).ok_or_else(|| format!("{publication}/{name} is not held")) +} + +fn catalog_outcome(case: &Value, held: &BTreeMap>, doc: &Value) -> Result { + let p = str_of(case, "publication")?; + let os = case["objects"] + .as_array() + .ok_or("objects")? + .iter() + .map(|o| object_named(doc, p, o.as_str().ok_or("a name")?, held)) + .collect::, _>>()?; + Ok(match check_catalog(str_of(case, "entry")?, &os) { + Ok(()) => json!({"outcome": "accept"}), + Err(e) => refusal(e), + }) +} + +/// Every claim's verification, in the document's order. +fn claim_results(doc: &Value, held: &BTreeMap>) -> Result)>, String> { + let mut out: Vec<(String, Result)> = Vec::new(); + let mut wires: BTreeMap = BTreeMap::new(); + for case in doc["claims"].as_array().ok_or("no claims")? { + let name = str_of(case, "name")?.to_owned(); + let wire = str_of(case, "wire")?.to_owned(); + let ctx = &case["context"]; + let mut objects = BTreeMap::new(); + for p in ctx["publications"].as_array().ok_or("publications")? { + if let Some(m) = held.get(p.as_str().ok_or("a name")?) { + objects.extend(m.iter().map(|(k, v)| (*k, v.clone()))); + } + } + let room = match ctx["room"].as_str() { + Some(r) => Some(room_of(doc, r)?), + None => None, + }; + let previous = match ctx["previous"].as_str() { + Some(p) => { + let w = wires.get(p).ok_or_else(|| format!("{name}: {p} comes first"))?; + Some(ClaimReceipt::from_json(w).map_err(|e| format!("{p}: {e}"))?) + } + None => None, + }; + let r = verify_claim(&wire, &ClaimContext { objects: &objects, room: room.as_ref(), previous: previous.as_ref() }); + wires.insert(name.clone(), wire); + out.push((name, r)); + } + Ok(out) +} + +fn claim_outcome(r: &Result) -> Value { + match r { + Ok(a) => json!({"outcome": "accept", "objects": a.outcomes.iter().map(|(h, o)| json!({"object": hex(h), "outcome": o.label()})).collect::>()}), + Err(e) => json!({"outcome": "refuse", "step": e.step, "reason": e.reason.code()}), + } +} + +fn gate_outcome(doc: &Value, case: &Value, claims: &[(String, Result)]) -> Result { + let p = &case["policy"]; + let window = p["window"].as_u64().filter(|w| *w <= MAX_WINDOW as u64).ok_or("a window")? as u8; + let policy = Policy { window, require_allowed: p["require_allowed"].as_bool().ok_or("require_allowed")? }; + let agent = unhex32(doc["keys"][str_of(case, "agent")?].as_str().ok_or("an agent key")?).ok_or("hex")?; + let hashes = |v: &Value| -> Result, String> { + v.as_array().ok_or("a list")?.iter().map(|h| h.as_str().and_then(unhex32).ok_or_else(|| "a hash".to_string())).collect() + }; + let served = case["served"].as_array().ok_or("served")?.iter().map(hashes).collect::, _>>()?; + let given: Vec> = case["claims"] + .as_array() + .ok_or("claims")? + .iter() + .map(|n| claims.iter().find(|(c, _)| Some(c.as_str()) == n.as_str()).map(|x| x.1.clone()).ok_or_else(|| format!("no claim {n}"))) + .collect::>()?; + Ok(match gate(policy, &agent, &served, &given, &hashes(&case["request"])?) { + Ok(()) => json!({"outcome": "serve"}), + Err(e) => refusal(e), + }) +} + +/// The outcome fields of every case, section by section, in the document's order. +fn evaluate(doc: &Value) -> Result>, String> { + let mut out: BTreeMap<&'static str, Vec> = BTreeMap::new(); + let cases = |k: &str| doc[k].as_array().cloned().ok_or_else(|| format!("no {k}")); + out.insert("statements", cases("statements")?.iter().map(statement_outcome).collect::>()?); + let gs = groups(doc)?; + out.insert("objects", cases("objects")?.iter().map(|c| object_outcome(c, &gs)).collect::>()?); + out.insert("rooms", cases("rooms")?.iter().map(|c| room_outcome(doc, c)).collect::>()?); + let held = held_by_publication(doc)?; + out.insert("responses", cases("responses")?.iter().map(|c| response_outcome(c, &held, doc)).collect::>()?); + out.insert("catalogs", cases("catalogs")?.iter().map(|c| catalog_outcome(c, &held, doc)).collect::>()?); + let claims = claim_results(doc, &held)?; + out.insert("claims", claims.iter().map(|(_, r)| claim_outcome(r)).collect()); + out.insert("gates", cases("gates")?.iter().map(|c| gate_outcome(doc, c, &claims)).collect::>()?); + Ok(out) +} + +const SECTIONS: [&str; 7] = ["statements", "objects", "rooms", "responses", "catalogs", "claims", "gates"]; + +/// The short outcome of a case: `accept`, `held`, `serve`, or its reason. A room's is the +/// first refusal of its records, else of its grants' terms, else `accept`. +fn short(section: &str, outcome: &Value) -> String { + if section == "rooms" { + if let Some(r) = outcome["records"]["reason"].as_str() { + return r.into(); + } + if let Some(r) = outcome["grants"]["reason"].as_str() { + return r.into(); + } + if let Some(r) = outcome["terms"].as_array().and_then(|t| t.iter().find_map(|x| x["terms"].as_str().filter(|s| s.len() != 64))) { + return r.into(); + } + return "accept".into(); + } + outcome["reason"].as_str().or(outcome["outcome"].as_str()).unwrap_or("?").to_owned() +} + +/// Write the corpus: `vectors.json`. +pub fn generate() -> Vec<(String, String)> { + let c = corpus(); + let keys: Map = KEYS.iter().map(|(n, i)| (n.to_string(), hex(&pk(*i)).into())).collect(); + let publications: Vec = c + .publications + .iter() + .map(|p| json!({"name": p.name, "room": p.room, "signer": KEYS.iter().find(|k| k.1 == p.signer).expect("a key").0, "group": p.group.to_json()})) + .collect(); + let rooms: Vec = c + .rooms + .iter() + .map(|r| { + let expect = match r.name { + "lecture" => "accept", + "record_of_another_room" => "wrong_room", + "record_not_the_authoritys" => "bad_signature", + "record_unlinked" | "record_not_later" => "room_usage_unlinked", + "record_not_canonical" | "record_statement_malformed" => "usage_malformed", + "record_truncated" => "truncated", + "record_unknown_version" => "unknown_version", + "record_wrong_magic" => "malformed", + "grant_terms_malformed" => "grant_usage_malformed", + other => panic!("no expectation for room {other}"), + }; + room_inputs(r, expect) + }) + .collect(); + let mut doc = json!({ + "extension": "ai-usage", + "revisions": {"aipref": AIPREF_REVISION, "cawg": CAWG_REVISION, "tdmrep": TDMREP_REVISION}, + "keys": keys, + "statements": statement_cases(), + "publications": publications, + "objects": object_inputs(&c), + "rooms": rooms, + "responses": response_inputs(), + "catalogs": catalog_inputs(), + "claims": claim_inputs(&c), + "gates": gate_inputs(&c), + }); + let outcomes = evaluate(&doc).expect("the generated document evaluates"); + let mut reasons: BTreeSet = BTreeSet::new(); + for section in SECTIONS { + let cases = doc[section].as_array_mut().expect("a section"); + for (case, out) in cases.iter_mut().zip(&outcomes[section]) { + let got = short(section, out); + let want = case.as_object_mut().expect("a case").remove("expect").expect("an expectation"); + assert_eq!(got, want.as_str().expect("a string"), "{section}/{}", case["name"]); + reasons.insert(got); + case["outcome"] = out.clone(); + } + } + for code in CODES { + assert!(reasons.contains(code), "the corpus gives no case refused {code}"); + } + vec![("vectors.json".into(), serde_json::to_string_pretty(&doc).expect("json") + "\n")] +} + +/// Check a corpus directory's files: every case's stated outcome is the one this module gives +/// from the document's inputs. The failures, each a line. +pub fn verify(files: &BTreeMap) -> Vec { + let Some(text) = files.get("vectors.json") else { return vec!["no vectors.json".into()] }; + let doc: Value = match serde_json::from_str(text) { + Ok(d) => d, + Err(e) => return vec![format!("vectors.json: {e}")], + }; + let outcomes = match evaluate(&doc) { + Ok(o) => o, + Err(e) => return vec![format!("vectors.json: {e}")], + }; + let mut fails = Vec::new(); + for section in SECTIONS { + let Some(cases) = doc[section].as_array() else { + fails.push(format!("no {section}")); + continue; + }; + for (case, out) in cases.iter().zip(&outcomes[section]) { + if case["outcome"] != *out { + fails.push(format!("{section}/{}: stated {}, got {}", case["name"], case["outcome"], out)); + } + } + } + fails +} diff --git a/wai/wai-rs/src/ai_usage/sf.rs b/wai/wai-rs/src/ai_usage/sf.rs new file mode 100644 index 000000000..8496cd8fe --- /dev/null +++ b/wai/wai-rs/src/ai_usage/sf.rs @@ -0,0 +1,391 @@ +//! RFC 9651 §4.2: parsing a Structured Field of type Dictionary, as ai-usage §3.3 reads an +//! `aipref` value and an HTTP `Content-Usage` field. +//! +//! Every bare item type is parsed, because whether a value parses decides whether a +//! statement is refused; only a member's being the Token `y` or `n` is kept, which is all +//! `draft-ietf-aipref-vocab-08` §6.5 reads. RFC 9651 leaves two checks of a Byte Sequence to +//! the parser (§4.2.7); ai-usage §3.3 fixes them: the content is base64 alphabet characters +//! then at most two `=`, and the alphabet characters do not number one more than a multiple +//! of four. + +use std::collections::BTreeMap; + +/// What a Dictionary member's value is, as far as the vocabulary reads it. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Member { + /// A Token, its text. + Token(String), + /// Any other bare item, or an inner list. + Other, +} + +/// A parsed Dictionary: each key's last value (§4.2.2 overwrites a repeated key). +pub type Dictionary = BTreeMap; + +struct P<'a> { + b: &'a [u8], + i: usize, +} + +type R = Result; + +fn is_lcalpha(c: u8) -> bool { + c.is_ascii_lowercase() +} + +fn is_tchar(c: u8) -> bool { + c.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&c) +} + +impl P<'_> { + fn peek(&self) -> Option { + self.b.get(self.i).copied() + } + + fn empty(&self) -> bool { + self.i >= self.b.len() + } + + fn eat(&mut self, c: u8) -> R<()> { + if self.peek() == Some(c) { + self.i += 1; + Ok(()) + } else { + Err(()) + } + } + + fn sp(&mut self) { + while self.peek() == Some(b' ') { + self.i += 1; + } + } + + fn ows(&mut self) { + while matches!(self.peek(), Some(b' ' | b'\t')) { + self.i += 1; + } + } + + /// §4.2.2. + fn dictionary(&mut self) -> R { + let mut d = Dictionary::new(); + while !self.empty() { + let key = self.key()?; + let member = if self.peek() == Some(b'=') { + self.i += 1; + self.item_or_inner_list()? + } else { + self.parameters()?; + Member::Other + }; + d.insert(key, member); + self.ows(); + if self.empty() { + return Ok(d); + } + self.eat(b',')?; + self.ows(); + if self.empty() { + return Err(()); + } + } + Ok(d) + } + + /// §4.2.1.1. + fn item_or_inner_list(&mut self) -> R { + if self.peek() == Some(b'(') { + self.inner_list()?; + Ok(Member::Other) + } else { + self.item() + } + } + + /// §4.2.1.2. + fn inner_list(&mut self) -> R<()> { + self.eat(b'(')?; + while !self.empty() { + self.sp(); + if self.peek() == Some(b')') { + self.i += 1; + return self.parameters(); + } + self.item()?; + if !matches!(self.peek(), Some(b' ' | b')')) { + return Err(()); + } + } + Err(()) + } + + /// §4.2.3. + fn item(&mut self) -> R { + let m = self.bare_item()?; + self.parameters()?; + Ok(m) + } + + /// §4.2.3.1. + fn bare_item(&mut self) -> R { + match self.peek() { + Some(c) if c == b'-' || c.is_ascii_digit() => self.number().map(|_| Member::Other), + Some(b'"') => self.string().map(|_| Member::Other), + Some(c) if c.is_ascii_alphabetic() || c == b'*' => self.token().map(Member::Token), + Some(b':') => self.byte_sequence().map(|_| Member::Other), + Some(b'?') => self.boolean().map(|_| Member::Other), + Some(b'@') => self.date().map(|_| Member::Other), + Some(b'%') => self.display_string().map(|_| Member::Other), + _ => Err(()), + } + } + + /// §4.2.3.2. + fn parameters(&mut self) -> R<()> { + while self.peek() == Some(b';') { + self.i += 1; + self.sp(); + self.key()?; + if self.peek() == Some(b'=') { + self.i += 1; + self.bare_item()?; + } + } + Ok(()) + } + + /// §4.2.3.3. + fn key(&mut self) -> R { + match self.peek() { + Some(c) if is_lcalpha(c) || c == b'*' => {} + _ => return Err(()), + } + let s = self.i; + while let Some(c) = self.peek() { + if !(is_lcalpha(c) || c.is_ascii_digit() || b"_-.*".contains(&c)) { + break; + } + self.i += 1; + } + Ok(String::from_utf8(self.b[s..self.i].to_vec()).expect("ASCII")) + } + + /// §4.2.4: Integer or Decimal. Returns whether it was an Integer. + fn number(&mut self) -> R { + let mut integer = true; + let mut digits: Vec = Vec::new(); + if self.peek() == Some(b'-') { + self.i += 1; + } + if self.empty() { + return Err(()); + } + if !self.peek().is_some_and(|c| c.is_ascii_digit()) { + return Err(()); + } + while let Some(c) = self.peek() { + self.i += 1; + if c.is_ascii_digit() { + digits.push(c); + } else if integer && c == b'.' { + if digits.len() > 12 { + return Err(()); + } + digits.push(c); + integer = false; + } else { + self.i -= 1; + break; + } + if integer && digits.len() > 15 { + return Err(()); + } + if !integer && digits.len() > 16 { + return Err(()); + } + } + if !integer { + if digits.last() == Some(&b'.') { + return Err(()); + } + let dot = digits.iter().position(|&c| c == b'.').expect("a decimal has its point"); + if digits.len() - dot - 1 > 3 { + return Err(()); + } + } + Ok(integer) + } + + /// §4.2.5. + fn string(&mut self) -> R<()> { + self.eat(b'"')?; + while let Some(c) = self.peek() { + self.i += 1; + match c { + b'\\' => { + let n = self.peek().ok_or(())?; + self.i += 1; + if n != b'"' && n != b'\\' { + return Err(()); + } + } + b'"' => return Ok(()), + 0x00..=0x1f | 0x7f..=0xff => return Err(()), + _ => {} + } + } + Err(()) + } + + /// §4.2.6. + fn token(&mut self) -> R { + match self.peek() { + Some(c) if c.is_ascii_alphabetic() || c == b'*' => {} + _ => return Err(()), + } + let s = self.i; + while let Some(c) = self.peek() { + if !(is_tchar(c) || c == b':' || c == b'/') { + break; + } + self.i += 1; + } + Ok(String::from_utf8(self.b[s..self.i].to_vec()).expect("ASCII")) + } + + /// §4.2.7, with ai-usage §3.3's reading of its content. + fn byte_sequence(&mut self) -> R<()> { + self.eat(b':')?; + let rest = &self.b[self.i..]; + let end = rest.iter().position(|&c| c == b':').ok_or(())?; + let content = &rest[..end]; + self.i += end + 1; + let alphabet = content.iter().take_while(|&&c| c.is_ascii_alphanumeric() || c == b'+' || c == b'/').count(); + let padding = &content[alphabet..]; + if padding.len() > 2 || padding.iter().any(|&c| c != b'=') || alphabet % 4 == 1 { + return Err(()); + } + Ok(()) + } + + /// §4.2.8. + fn boolean(&mut self) -> R<()> { + self.eat(b'?')?; + match self.peek() { + Some(b'0' | b'1') => { + self.i += 1; + Ok(()) + } + _ => Err(()), + } + } + + /// §4.2.9. + fn date(&mut self) -> R<()> { + self.eat(b'@')?; + if self.number()? { Ok(()) } else { Err(()) } + } + + /// §4.2.10. + fn display_string(&mut self) -> R<()> { + self.eat(b'%')?; + self.eat(b'"')?; + let mut bytes = Vec::new(); + while let Some(c) = self.peek() { + self.i += 1; + match c { + 0x00..=0x1f | 0x7f..=0xff => return Err(()), + b'%' => { + let h = self.b.get(self.i..self.i + 2).ok_or(())?; + if !h.iter().all(|&x| x.is_ascii_digit() || (b'a'..=b'f').contains(&x)) { + return Err(()); + } + let s = std::str::from_utf8(h).map_err(|_| ())?; + bytes.push(u8::from_str_radix(s, 16).map_err(|_| ())?); + self.i += 2; + } + b'"' => return String::from_utf8(bytes).map(|_| ()).map_err(|_| ()), + _ => bytes.push(c), + } + } + Err(()) + } +} + +/// Parse `text` as a Dictionary field value (§4.2), or `None` when it fails to parse. +pub fn parse_dictionary(text: &str) -> Option { + if !text.is_ascii() { + return None; + } + let mut p = P { b: text.as_bytes(), i: 0 }; + p.sp(); + let d = p.dictionary().ok()?; + p.sp(); + p.empty().then_some(d) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn tok(s: &str) -> Member { + Member::Token(s.into()) + } + + #[test] + fn the_vocabulary_examples() { + // draft-ietf-aipref-vocab-08 §6. + let d = parse_dictionary("train-ai=y, search=n").unwrap(); + assert_eq!(d["train-ai"], tok("y")); + assert_eq!(d["search"], tok("n")); + // §6.5.1: the last value applies; a boolean and a string are not tokens. + let d = parse_dictionary(r#"train-ai=y, train-ai, search=n, search="n""#).unwrap(); + assert_eq!(d["train-ai"], Member::Other); + assert_eq!(d["search"], Member::Other); + // §6.5.2: parameters are disregarded, and only those of the selected value apply. + let d = parse_dictionary("train-ai;allow=n, train-ai=y").unwrap(); + assert_eq!(d["train-ai"], tok("y")); + assert!(parse_dictionary(r#"train-ai;has;parameters="?";"#).is_none(), "a trailing ';' has no key"); + assert!(parse_dictionary(r#"train-ai;has;parameters="?""#).is_some()); + // §6.5.1: upper case fails the Dictionary. + assert!(parse_dictionary("Train-AI=y").is_none()); + } + + #[test] + fn rfc9651_edges() { + assert_eq!(parse_dictionary("").unwrap().len(), 0); + assert!(parse_dictionary("a=1,").is_none(), "a trailing comma"); + assert!(parse_dictionary("a=1 ,\tb=2").is_some()); + assert!(parse_dictionary("a=1 ").is_some()); + assert!(parse_dictionary("\ta=1").is_none(), "only spaces lead"); + assert!(parse_dictionary("a=(1 2);x, b=?0").is_some()); + assert!(parse_dictionary("a=(1 2").is_none()); + assert!(parse_dictionary("a=(1,2)").is_none()); + assert!(parse_dictionary("a=1234567890123456").is_none(), "16 integer digits"); + assert!(parse_dictionary("a=123456789012345").is_some()); + assert!(parse_dictionary("a=1234567890123.1").is_none(), "13 digits before the point"); + assert!(parse_dictionary("a=123456789012.123").is_some()); + assert!(parse_dictionary("a=1.1234").is_none()); + assert!(parse_dictionary("a=1.").is_none()); + assert!(parse_dictionary("a=-").is_none()); + assert!(parse_dictionary("a=@1700000000, b=@1.5").is_none(), "a date is an integer"); + assert!(parse_dictionary("a=@-1700000000").is_some()); + assert!(parse_dictionary(r#"a=%"caf%c3%a9""#).is_some()); + assert!(parse_dictionary(r#"a=%"caf%C3%A9""#).is_none(), "lower-case hex only"); + assert!(parse_dictionary(r#"a=%"%c3""#).is_none(), "not UTF-8"); + assert!(parse_dictionary(r#"a="x\y""#).is_none()); + assert!(parse_dictionary(r#"a="x\\y\"z""#).is_some()); + assert!(parse_dictionary("a=:aGVsbG8=:").is_some()); + assert!(parse_dictionary("a=:aGVsbG8:").is_some(), "padding is not checked"); + assert!(parse_dictionary("a=:aGVsb:").is_none(), "five characters"); + assert!(parse_dictionary("a=:aGV=sbG8:").is_none()); + assert!(parse_dictionary("a=:aGVsbG8===:").is_none()); + assert!(parse_dictionary("a=:aGVsbG8").is_none()); + assert!(parse_dictionary("a=?2").is_none()); + assert!(parse_dictionary("a=*tok/en:x").unwrap()["a"] == tok("*tok/en:x")); + assert!(parse_dictionary("a=b, *c=d").is_some()); + assert!(parse_dictionary("1a=b").is_none()); + assert!(parse_dictionary("a=é").is_none()); + } +} diff --git a/wai/wai-rs/src/bin/wai_ai_usage_vectors.rs b/wai/wai-rs/src/bin/wai_ai_usage_vectors.rs new file mode 100644 index 000000000..0ab3676c8 --- /dev/null +++ b/wai/wai-rs/src/bin/wai_ai_usage_vectors.rs @@ -0,0 +1,74 @@ +//! Conformance corpus for AI usage preferences and consumption claims (`live-conformance/usage/`, extensions/ai-usage.md §10). +//! +//! wai_ai_usage_vectors gen write vectors.json, from fixed seeds +//! wai_ai_usage_vectors verify every file regenerates byte for byte, no other +//! file but README.md is there, and every case gets +//! the outcome it states +//! +//! The generator and this verifier are one implementation. The corpus's second +//! implementation is `live-conformance/ai_usage_verify.py`, written from the extension's +//! text and sharing no code with this binary. + +use std::collections::BTreeMap; +use std::path::Path; +use wai::ai_usage::corpus; + +fn main() { + let a: Vec = std::env::args().collect(); + match (a.get(1).map(String::as_str), a.get(2)) { + (Some("gen"), Some(d)) => { + if let Err(e) = std::fs::create_dir_all(d) { + eprintln!("create {d}: {e}"); + std::process::exit(1); + } + for (name, body) in corpus::generate() { + let p = Path::new(d).join(&name); + if let Err(e) = std::fs::write(&p, body) { + eprintln!("write {}: {e}", p.display()); + std::process::exit(1); + } + } + println!("wrote {d}"); + } + (Some("verify"), Some(d)) => std::process::exit(verify(Path::new(d))), + _ => { + eprintln!("usage: wai_ai_usage_vectors gen|verify "); + std::process::exit(2); + } + } +} + +fn verify(dir: &Path) -> i32 { + let mut files = BTreeMap::new(); + let entries = match std::fs::read_dir(dir) { + Ok(e) => e, + Err(e) => { + eprintln!("read {}: {e}", dir.display()); + return 1; + } + }; + for e in entries.flatten() { + let name = e.file_name().to_string_lossy().into_owned(); + if name == "README.md" { + continue; + } + match std::fs::read_to_string(e.path()) { + Ok(s) => { + files.insert(name, s); + } + Err(err) => { + eprintln!("read {name}: {err}"); + return 1; + } + } + } + let generated: BTreeMap = corpus::generate().into_iter().collect(); + let same = files == generated; + println!("{} every file regenerates byte for byte from its seeds, and no other file is there", if same { "PASS" } else { "FAIL" }); + let fails = corpus::verify(&files); + for f in &fails { + println!("FAIL {f}"); + } + println!("{} every case gets the outcome the corpus states", if fails.is_empty() { "PASS" } else { "FAIL" }); + (!same || !fails.is_empty()) as i32 +} diff --git a/wai/wai-rs/src/claim_receipt.rs b/wai/wai-rs/src/claim_receipt.rs index 09744d940..e02945611 100644 --- a/wai/wai-rs/src/claim_receipt.rs +++ b/wai/wai-rs/src/claim_receipt.rs @@ -127,6 +127,8 @@ pub const DOMAIN_CLAIM_LINK: &[u8] = b"moq-jwp:claim-link\x01"; // names here so every existing caller of this module's API stays valid. pub use crate::jwp_hash::{content_hash as object_content_hash, DOMAIN_OBJECT}; +pub mod consumption; +pub use consumption::{ConsumptionClaim, UsageEntry, Vocabulary}; pub mod session; pub use session::{ DecoderEntry, Missed, ObjectSeen, SessionClaim, SessionEnergy, SessionHost, SessionPath, StageSetRef, Switch, @@ -383,6 +385,19 @@ pub enum Reason { /// A `stage` claim the session names counts units the session's outputs do not hold /// at its stages. SessionStageDisagrees, + /// A `consumption` claim's `objects` are not in strictly ascending order. + ConsumptionObjectsUnordered, + /// A `consumption` claim's `subject` is not the first of its `objects`. + ConsumptionSubjectMismatch, + /// A `consumption` claim's `usage` entries are not in strictly ascending order of + /// vocabulary. + ConsumptionUsageUnordered, + /// A `consumption` claim names a term its vocabulary does not define. + ConsumptionTermUnknown, + /// A `consumption` claim's `seen` digests are not in strictly ascending order. + ConsumptionSeenUnordered, + /// A `consumption` claim's `signer_id` is not the `did:key` of its `signer_pubkey`. + ConsumptionSignerNotPrincipal, } impl Reason { @@ -487,6 +502,12 @@ impl Reason { Reason::SessionUnitOutsideObject => "session_unit_outside_object", Reason::SessionClassicalMismatch => "session_classical_mismatch", Reason::SessionStageDisagrees => "session_stage_disagrees", + Reason::ConsumptionObjectsUnordered => "consumption_objects_unordered", + Reason::ConsumptionSubjectMismatch => "consumption_subject_mismatch", + Reason::ConsumptionUsageUnordered => "consumption_usage_unordered", + Reason::ConsumptionTermUnknown => "consumption_term_unknown", + Reason::ConsumptionSeenUnordered => "consumption_seen_unordered", + Reason::ConsumptionSignerNotPrincipal => "consumption_signer_not_principal", } } } @@ -557,6 +578,7 @@ pub enum ClaimClass { Stage, DeliveryGap, ReconstructSession, + Consumption, } /// The class of `group` and `object` receipts. A claim naming it is rejected. @@ -575,6 +597,7 @@ impl ClaimClass { ClaimClass::Stage => "stage", ClaimClass::DeliveryGap => "delivery-gap", ClaimClass::ReconstructSession => "reconstruct-session", + ClaimClass::Consumption => "consumption", } } @@ -590,6 +613,7 @@ impl ClaimClass { "stage" => Some(ClaimClass::Stage), "delivery-gap" => Some(ClaimClass::DeliveryGap), "reconstruct-session" => Some(ClaimClass::ReconstructSession), + "consumption" => Some(ClaimClass::Consumption), _ => None, } } @@ -610,7 +634,8 @@ impl ClaimClass { | ClaimClass::BindingRecovery | ClaimClass::Stage | ClaimClass::DeliveryGap - | ClaimClass::ReconstructSession => t == SubjectType::Object, + | ClaimClass::ReconstructSession + | ClaimClass::Consumption => t == SubjectType::Object, } } } @@ -1572,6 +1597,7 @@ pub enum ClaimBody { Stage(StageClaim), DeliveryGap(DeliveryGapClaim), ReconstructSession(SessionClaim), + Consumption(ConsumptionClaim), } /// The energy body's members, in the order step 2 reads them. @@ -1665,6 +1691,7 @@ impl ClaimBody { ClaimBody::Stage(_) => ClaimClass::Stage, ClaimBody::DeliveryGap(_) => ClaimClass::DeliveryGap, ClaimBody::ReconstructSession(_) => ClaimClass::ReconstructSession, + ClaimBody::Consumption(_) => ClaimClass::Consumption, } } @@ -1821,6 +1848,7 @@ impl ClaimBody { m.insert("observed_at".into(), g.observed_at.clone().into()); } ClaimBody::ReconstructSession(c) => return c.to_value(), + ClaimBody::Consumption(c) => return c.to_value(), } Value::Object(m) } @@ -2044,6 +2072,7 @@ impl ClaimBody { ClaimBody::DeliveryGap(DeliveryGapClaim { track_alias, group_id, object_id, reason, observed_at }) } ClaimClass::ReconstructSession => ClaimBody::ReconstructSession(SessionClaim::read(m)?), + ClaimClass::Consumption => ClaimBody::Consumption(ConsumptionClaim::read(m)?), ClaimClass::BindingRecovery => { members(m, RECOVERY_MEMBERS)?; ClaimBody::BindingRecovery(BindingRecoveryClaim { @@ -2177,11 +2206,18 @@ impl ClaimBody { Ok(()) } ClaimBody::ReconstructSession(c) => c.check_values(), + ClaimBody::Consumption(c) => c.check_values(), } } /// The class rules (step 4), in the order §3.1 lists them. - fn check_class_rules(&self, subject_type: SubjectType, subject: &[u8; 32]) -> Result<(), Reason> { + fn check_class_rules( + &self, + subject_type: SubjectType, + subject: &[u8; 32], + signer_pubkey: &[u8; 32], + signer_id: &str, + ) -> Result<(), Reason> { match self { ClaimBody::Measurement(c) => { let class = c.figure.check_rules(subject_type)?; @@ -2275,6 +2311,7 @@ impl ClaimBody { // against the object receipt at its coordinates at step 6. ClaimBody::DeliveryGap(_) => Ok(()), ClaimBody::ReconstructSession(c) => c.rules(), + ClaimBody::Consumption(c) => c.rules(subject, signer_pubkey, signer_id), } } } @@ -2439,7 +2476,9 @@ impl ClaimReceipt { sig: [0u8; 64], }; r.check_shape()?; - r.body.check_class_rules(r.subject_type, &r.subject).map_err(|e| Rejection::at(4, e))?; + r.body + .check_class_rules(r.subject_type, &r.subject, &r.signer_pubkey, &r.signer_id) + .map_err(|e| Rejection::at(4, e))?; r.sig = signer.sign(&r.signing_input()?).to_bytes(); Ok(r) } @@ -2529,7 +2568,7 @@ impl ClaimReceipt { crate::sig::verify_strict(&self.signer_pubkey, &self.signing_input()?, &self.sig) .map_err(|e| Rejection::at(3, Reason::of_signature(e)))?; self.body - .check_class_rules(self.subject_type, &self.subject) + .check_class_rules(self.subject_type, &self.subject, &self.signer_pubkey, &self.signer_id) .map_err(|r| Rejection::at(4, r)) } diff --git a/wai/wai-rs/src/claim_receipt/consumption.rs b/wai/wai-rs/src/claim_receipt/consumption.rs new file mode 100644 index 000000000..00f38fca9 --- /dev/null +++ b/wai/wai-rs/src/claim_receipt/consumption.rs @@ -0,0 +1,229 @@ +//! `consumption` (jwp-receipts §2.4.13; `extensions/ai-usage.md` §6): an agent's statement of +//! the objects it consumed, the use it made of them in each AI usage vocabulary it answers, +//! the usage statements it saw and the room grant it held. +//! +//! This file holds steps 2 and 4 of jwp-receipts §3.1: the body's members and their types, +//! and the class rules of ai-usage §6.2. Step 6, the check against the objects, the +//! statements and the room, is `crate::ai_usage`'s, which holds them. + +use super::{get_opt_hash, hex, members, unhex, Reason}; +use serde_json::{Map, Value}; + +/// The body's members, in the order step 2 reads them (ai-usage §6.1). +const CONSUMPTION_MEMBERS: &[&str] = &["objects", "usage", "seen", "grant"]; +/// A `usage` entry's members. +const USAGE_MEMBERS: &[&str] = &["vocabulary", "term"]; +/// The most objects one claim names. +pub const MAX_OBJECTS: usize = 1024; +/// The most statements one claim says it saw. +pub const MAX_SEEN: usize = 64; +/// The longest term, in bytes. +pub const MAX_TERM: usize = 255; + +/// The three vocabularies a usage statement carries (ai-usage §3.1), in the order a claim +/// lists them: ascending by name. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Vocabulary { + /// `draft-ietf-aipref-vocab-08`. + Aipref, + /// The Training and Data Mining Assertion, version 1.1. + Cawg, + /// The TDM Reservation Protocol. + Tdmrep, +} + +impl Vocabulary { + pub const ALL: [Vocabulary; 3] = [Vocabulary::Aipref, Vocabulary::Cawg, Vocabulary::Tdmrep]; + + pub fn label(self) -> &'static str { + match self { + Vocabulary::Aipref => "aipref", + Vocabulary::Cawg => "cawg", + Vocabulary::Tdmrep => "tdmrep", + } + } + + pub fn from_label(s: &str) -> Option { + Vocabulary::ALL.into_iter().find(|v| v.label() == s) + } + + /// Whether `term` is a term of this vocabulary (ai-usage §6.2 rule 4). + pub fn has_term(self, term: &str) -> bool { + match self { + Vocabulary::Aipref => AIPREF_LABELS.contains(&term), + Vocabulary::Tdmrep => term == TDM_TERM, + Vocabulary::Cawg => CAWG_KEYS.contains(&term) || (cawg_key_form(term) && !term.starts_with("cawg.")), + } + } +} + +/// The usage category labels of `draft-ietf-aipref-vocab-08` §6.1, Table 1. +pub const AIPREF_LABELS: [&str; 3] = ["train-ai", "ai-use", "search"]; +/// The one term of the TDM Reservation Protocol: the property that governs mining. +pub const TDM_TERM: &str = "tdm-reservation"; +/// The four entries the Training and Data Mining Assertion 1.1 defines (§3.1 there). +pub const CAWG_KEYS: [&str; 4] = ["cawg.data_mining", "cawg.ai_inference", "cawg.ai_generative_training", "cawg.ai_training"]; + +/// ai-usage §3.4: 1 to 255 bytes of components separated by single dots, each one or more +/// of `a`–`z`, `A`–`Z`, `0`–`9`, `_` and `-`. +pub fn cawg_key_form(k: &str) -> bool { + !k.is_empty() + && k.len() <= 255 + && k.split('.').all(|c| !c.is_empty() && c.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')) +} + +/// One `usage` entry: the use, as a vocabulary names it, or `None` for a use outside it. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct UsageEntry { + pub vocabulary: Vocabulary, + pub term: Option, +} + +/// A `consumption` body. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ConsumptionClaim { + /// The content hashes of the objects consumed. + pub objects: Vec<[u8; 32]>, + /// The use, in each vocabulary the agent answers. + pub usage: Vec, + /// The digests of the statements that governed the objects. + pub seen: Vec<[u8; 32]>, + /// The digest of the room grant the agent consumed under. + pub grant: Option<[u8; 32]>, +} + +fn strictly_ascending(v: &[T]) -> bool { + v.windows(2).all(|w| w[0] < w[1]) +} + +impl ConsumptionClaim { + pub(super) fn to_value(&self) -> Value { + let mut m = Map::new(); + m.insert("objects".into(), Value::Array(self.objects.iter().map(|h| hex(h).into()).collect())); + m.insert( + "usage".into(), + Value::Array( + self.usage + .iter() + .map(|u| { + let mut e = Map::new(); + e.insert("vocabulary".into(), u.vocabulary.label().into()); + e.insert("term".into(), u.term.clone().map_or(Value::Null, Value::String)); + Value::Object(e) + }) + .collect(), + ), + ); + m.insert("seen".into(), Value::Array(self.seen.iter().map(|h| hex(h).into()).collect())); + m.insert("grant".into(), self.grant.map_or(Value::Null, |g| hex(&g).into())); + Value::Object(m) + } + + /// Step 2: the members — missing, then unknown — then each member's type in the table's + /// order, an entry's members and types where the table reaches it. + pub(super) fn read(m: &Map) -> Result { + members(m, CONSUMPTION_MEMBERS)?; + let ft = Reason::FieldType; + let hashes = |k: &str, least: usize, most: usize| -> Result, Reason> { + let a = m[k].as_array().ok_or(ft)?; + if a.len() < least || a.len() > most { + return Err(ft); + } + a.iter().map(|v| v.as_str().and_then(unhex).and_then(|b| b.try_into().ok()).ok_or(ft)).collect() + }; + let objects = hashes("objects", 1, MAX_OBJECTS)?; + let entries = m["usage"].as_array().ok_or(ft)?; + if entries.is_empty() || entries.len() > Vocabulary::ALL.len() { + return Err(ft); + } + let usage = entries + .iter() + .map(|v| { + let e = v.as_object().ok_or(ft)?; + members(e, USAGE_MEMBERS)?; + let vocabulary = Vocabulary::from_label(e["vocabulary"].as_str().ok_or(ft)?).ok_or(ft)?; + let term = match &e["term"] { + Value::Null => None, + Value::String(s) if !s.is_empty() && s.len() <= MAX_TERM => Some(s.clone()), + _ => return Err(ft), + }; + Ok(UsageEntry { vocabulary, term }) + }) + .collect::, Reason>>()?; + let seen = hashes("seen", 0, MAX_SEEN)?; + let grant = get_opt_hash(m, "grant")?; + Ok(ConsumptionClaim { objects, usage, seen, grant }) + } + + /// The counts and term lengths step 2 checks, for a body built field by field. + pub(super) fn check_values(&self) -> Result<(), Reason> { + let ft = Reason::FieldType; + if self.objects.is_empty() || self.objects.len() > MAX_OBJECTS || self.seen.len() > MAX_SEEN { + return Err(ft); + } + if self.usage.is_empty() || self.usage.len() > Vocabulary::ALL.len() { + return Err(ft); + } + if self.usage.iter().any(|u| u.term.as_ref().is_some_and(|t| t.is_empty() || t.len() > MAX_TERM)) { + return Err(ft); + } + Ok(()) + } + + /// Step 4: ai-usage §6.2's rules, in its order. + pub(super) fn rules(&self, subject: &[u8; 32], signer_pubkey: &[u8; 32], signer_id: &str) -> Result<(), Reason> { + if !strictly_ascending(&self.objects) { + return Err(Reason::ConsumptionObjectsUnordered); + } + if self.objects.first() != Some(subject) { + return Err(Reason::ConsumptionSubjectMismatch); + } + let vocabularies: Vec = self.usage.iter().map(|u| u.vocabulary).collect(); + if !strictly_ascending(&vocabularies) { + return Err(Reason::ConsumptionUsageUnordered); + } + if self.usage.iter().any(|u| u.term.as_ref().is_some_and(|t| !u.vocabulary.has_term(t))) { + return Err(Reason::ConsumptionTermUnknown); + } + if !strictly_ascending(&self.seen) { + return Err(Reason::ConsumptionSeenUnordered); + } + if crate::signer_id::did_key(signer_pubkey) != signer_id { + return Err(Reason::ConsumptionSignerNotPrincipal); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn terms_are_the_vocabularies_own() { + for t in AIPREF_LABELS { + assert!(Vocabulary::Aipref.has_term(t)); + } + assert!(!Vocabulary::Aipref.has_term("inference")); + assert!(!Vocabulary::Aipref.has_term("train-genai")); + assert!(Vocabulary::Tdmrep.has_term("tdm-reservation")); + assert!(!Vocabulary::Tdmrep.has_term("tdm-policy")); + for k in CAWG_KEYS { + assert!(Vocabulary::Cawg.has_term(k)); + } + assert!(Vocabulary::Cawg.has_term("org.example.summaries")); + assert!(!Vocabulary::Cawg.has_term("cawg.ai_summaries"), "the prefix is reserved"); + assert!(!Vocabulary::Cawg.has_term("org..example")); + assert!(!Vocabulary::Cawg.has_term("org example")); + assert!(!Vocabulary::Cawg.has_term("")); + assert!(!Vocabulary::Cawg.has_term(&"a".repeat(256))); + assert!(Vocabulary::Cawg.has_term(&"a".repeat(255))); + } + + #[test] + fn vocabularies_order_by_name() { + let mut v = Vocabulary::ALL.map(|x| x.label()); + v.sort(); + assert_eq!(v, Vocabulary::ALL.map(|x| x.label())); + } +} diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs index bdec9436b..cb73b6089 100644 --- a/wai/wai-rs/src/http_binding/receipts.rs +++ b/wai/wai-rs/src/http_binding/receipts.rs @@ -681,6 +681,9 @@ fn step6( // §2.4.12: its outputs, received objects and envelopes are the sink's, not // served beside an object. ClaimBody::ReconstructSession(_) => Err(ReceiptRefusal::ContextNotHeld), + // §2.4.13: its objects, the statements that governed them and its room are checked + // together (ai-usage §6.3), never one object at a time. + ClaimBody::Consumption(_) => Err(ReceiptRefusal::ContextNotHeld), _ => Ok(()), } } diff --git a/wai/wai-rs/src/lib.rs b/wai/wai-rs/src/lib.rs index 189e83f18..29d1b5c9a 100644 --- a/wai/wai-rs/src/lib.rs +++ b/wai/wai-rs/src/lib.rs @@ -477,6 +477,15 @@ pub mod live_watermark; /// cbindgen:ignore #[cfg(feature = "live_ads")] pub mod live_ads; +// AI usage preferences and consumption claims (extensions/ai-usage.md): `wai.ai.usage` — the +// usage statement carrying draft-ietf-aipref-vocab-08, the TDM Reservation Protocol and the +// Training and Data Mining Assertion verbatim, bound by an envelope's receipt, a room's WAU1 +// record and a grant's terms; the HTTP fields and catalog field checked against it; the one +// precedence rule; the consumption claim's context; and the gate. Rust-only, like the live +// modules: its constants must not become macros in include/wai.h. +/// cbindgen:ignore +#[cfg(feature = "ai_usage")] +pub mod ai_usage; // Timed text and live captions (extensions/timed-text.md): `wai.text.timed` — the cue model // both WebVTT and the IMSC1 Text Profile express, live caption revisions as `caption` // events of `wai.live.events` with the WTX1 annotation, and the exact mappings onto WebVTT diff --git a/wai/wai-rs/tests/ai_usage.rs b/wai/wai-rs/tests/ai_usage.rs new file mode 100644 index 000000000..2a0489c68 --- /dev/null +++ b/wai/wai-rs/tests/ai_usage.rs @@ -0,0 +1,61 @@ +//! `wai.ai.usage` (extensions/ai-usage.md): the committed corpus `live-conformance/usage/` +//! regenerates byte for byte from its fixed seeds, holds no other file, every case in it gets +//! the outcome it states, and the extension's §9 lists exactly the reasons the reference +//! reports. The generator itself asserts that the corpus gives every one of them. +#![cfg(feature = "ai_usage")] + +use std::collections::BTreeMap; +use std::path::PathBuf; +use wai::ai_usage::{corpus, CODES}; + +fn wai_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..") +} + +fn committed() -> BTreeMap { + let dir = wai_dir().join("live-conformance/usage"); + std::fs::read_dir(&dir) + .expect("the committed corpus") + .flatten() + .map(|e| (e.file_name().to_string_lossy().into_owned(), e.path())) + .filter(|(n, _)| n != "README.md") + .map(|(n, p)| (n, std::fs::read_to_string(p).expect("a text file"))) + .collect() +} + +#[test] +fn the_committed_corpus_regenerates_from_its_seeds() { + let files = committed(); + let generated: BTreeMap = corpus::generate().into_iter().collect(); + let extra: Vec<&String> = files.keys().filter(|k| !generated.contains_key(*k)).collect(); + let missing: Vec<&String> = generated.keys().filter(|k| !files.contains_key(*k)).collect(); + let differ: Vec<&String> = generated.iter().filter(|(k, v)| files.get(*k).is_some_and(|f| f != *v)).map(|(k, _)| k).collect(); + assert!(extra.is_empty() && missing.is_empty() && differ.is_empty(), "extra {extra:?}, missing {missing:?}, differ {differ:?}: run `wai_ai_usage_vectors gen`"); +} + +#[test] +fn every_case_gets_its_stated_outcome() { + let fails = corpus::verify(&committed()); + assert!(fails.is_empty(), "{}", fails.join("\n")); +} + +/// §9 of the extension lists exactly the codes `CODES` has, each once, in order. +#[test] +fn the_extension_lists_the_reasons() { + let text = std::fs::read_to_string(wai_dir().join("extensions/ai-usage.md")).expect("the extension"); + let at = text.find("\n## 9. Reasons").expect("a §9"); + let end = text[at + 1..].find("\n## ").map_or(text.len(), |e| at + 1 + e); + let listed: Vec = + text[at..end].lines().filter(|l| l.starts_with("| `")).filter_map(|l| l.split('`').nth(1).map(String::from)).collect(); + let want: Vec = CODES.iter().map(|c| c.to_string()).collect(); + assert_eq!(listed, want, "§9's table and CODES differ"); +} + +/// jwp-receipts §3.1's table lists the six class-rule codes of a `consumption` claim, at step 4. +#[test] +fn jwp_receipts_lists_the_class_rules() { + let text = std::fs::read_to_string(wai_dir().join("extensions/jwp-receipts.md")).expect("jwp-receipts"); + for code in CODES.iter().filter(|c| c.starts_with("consumption_") && **c != "consumption_object_unknown") { + assert!(text.contains(&format!("| 4 | `{code}` |")), "jwp-receipts §3.1 does not list {code} at step 4"); + } +} -- 2.54.0 (Apple Git-157) From d1a1fdb6b1f069c542f5ec3885689cd76e22d5c8 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Wed, 21 Oct 2026 05:23:29 -0400 Subject: [PATCH 2/7] wai.ai.usage: the class in jwp-receipts, the room and HTTP and MoQ bindings, registry, CI and the second implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit jwp-receipts registers the consumption class (§2.4.13) with its step 4 and step 6 reasons and its authority; live-rooms gains §3.8 and the ai_usage grant attribute; http-delivery §5.6 maps the signed statement onto the Content-Usage and TDMRep fields; moq-streaming-format §4 adds the wai_ai_usage catalog field. The registry export lists wai.ai.usage, WAU1, wai_ai_usage and the proposed WAI-Usage-Claim field. ai_usage_verify.py is the corpus's second implementation, and claim_verify.py reads the class's steps 1 to 4. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-conformance.yml | 33 +- wai/REGISTRY.md | 12 +- wai/SPEC.md | 23 +- wai/claim-conformance/claim_verify.py | 85 +- wai/extensions/ai-usage.md | 12 +- wai/extensions/http-delivery.md | 16 + wai/extensions/jwp-receipts.md | 39 +- wai/extensions/live-rooms.md | 18 +- wai/extensions/moq-streaming-format.md | 5 + wai/live-conformance/README.md | 12 +- wai/live-conformance/ai_usage_verify.py | 1228 +++++++++++++++++++ wai/live-conformance/usage/README.md | 44 + wai/registry/capabilities.json | 35 + wai/vocab/v2.ttl | 7 + wai/wai-rs/src/claim_receipt.rs | 1 + wai/wai-rs/src/claim_receipt/consumption.rs | 7 + wai/wai-rs/src/codecs/registry.rs | 11 +- 17 files changed, 1559 insertions(+), 29 deletions(-) create mode 100644 wai/live-conformance/ai_usage_verify.py create mode 100644 wai/live-conformance/usage/README.md diff --git a/.github/workflows/wai-conformance.yml b/.github/workflows/wai-conformance.yml index de429a1be..73ccaea66 100644 --- a/.github/workflows/wai-conformance.yml +++ b/.github/workflows/wai-conformance.yml @@ -106,6 +106,10 @@ on: # reports, live_ads_verify.py reads that table, and the registry's wire identifiers test # reads its sections. - 'wai/extensions/live-ads.md' + # tests/ai_usage.rs requires this extension's §9 to list the codes the reference reports, + # ai_usage_verify.py reads that table, and the registry's wire identifiers test reads its + # sections. + - 'wai/extensions/ai-usage.md' - 'wai/wai-web/**' # The audio provenance check below reads the corpora above with this tool. - 'wai/tools/wai_audio_provenance_check.py' @@ -776,7 +780,8 @@ jobs: # live_metering_verify.py (the live-metering corpus), energy_steering_verify.py (the # energy-steering corpus), and live_moderation_verify.py (the live-moderation corpus), and # e2ee_verify.py (the end-to-end encrypted rooms corpus), live_watermark_verify.py (the - # forensic-watermark corpus) and live_ads_verify.py (the live-ads corpus). + # forensic-watermark corpus), live_ads_verify.py (the live-ads corpus) and + # ai_usage_verify.py (the AI usage corpus). # Each exits non-zero on any failure or any difference, and the step fails with it. # One platform: what they check is the reading of the text, not the architecture. independent-verifiers: @@ -1091,6 +1096,32 @@ jobs: (cd wai-rs && cargo test --no-default-features --features live_ads --lib wai_video_ssai::) (cd wai-rs && cargo test --no-default-features --features live_ads --test live_ads) python3 -I live-conformance/live_ads_verify.py live-conformance/ads + # wai.ai.usage (extensions/ai-usage.md): the reference module's unit tests (RFC 9651's + # Dictionary parser against the vocabulary's examples and the RFC's edges, statements + # read and refused in their steps' order, the precedence rule, HTTP fields compared by + # meaning), the consumption class's body and terms in claim_receipt, and + # tests/ai_usage.rs (vectors.json regenerates byte for byte from its fixed seeds and no + # other file is there, wai-rs gives every statement, object, room, response, catalog, + # claim and gate its stated outcome, §9 lists exactly the reference's codes, each of which + # the corpus gives, and jwp-receipts §3.1 lists the class rules); then the corpus's second + # implementation, ai_usage_verify.py, written from the extension's text and sharing no + # code with wai-rs, which parses Structured Fields, canonical forms, receipts and claims + # itself, reads rooms with live_rooms_verify.py, checks signatures with WAI's strict + # Ed25519 rule and BLAKE3 from claim-conformance, reads the extension's reason table, + # requires every mutated copy of the corpus to fail, and drops each of its own checks in + # turn and requires the corpus to catch the weakened verifier; standard library only, run + # isolated (-I). Last, claim-conformance's own verifier reads every consumption claim of + # the corpus through jwp-receipts §3.1 steps 1 to 4 and requires the stated step and reason. + - name: AI usage · wai-rs and ai_usage_verify.py + working-directory: wai + shell: bash + run: | + set -euo pipefail + (cd wai-rs && cargo test --no-default-features --features ai_usage --lib ai_usage::) + (cd wai-rs && cargo test --no-default-features --features ai_usage --lib claim_receipt::consumption::) + (cd wai-rs && cargo test --no-default-features --features ai_usage --test ai_usage) + python3 -I live-conformance/ai_usage_verify.py live-conformance/usage + python3 -I claim-conformance/claim_verify.py consumption live-conformance/usage/vectors.json # wai.live.e2ee (extensions/confidentiality.md): the corpus's second implementation, # e2ee_verify.py, written from the extension's text and the documents it reads and # sharing no code with the reference (joule-moq's secure_objects and joule-moq-mls, diff --git a/wai/REGISTRY.md b/wai/REGISTRY.md index 5e28e271c..ceb352691 100644 --- a/wai/REGISTRY.md +++ b/wai/REGISTRY.md @@ -42,8 +42,8 @@ Every registered string has one kind. `wai.video.manifest` and its siblings; the live records `wai.live.events`, `wai.live.room`, `wai.capture.take`, `wai.live.compose`, `wai.live.recording`, `wai.live.metering`, `wai.live.steering`, `wai.live.moderation`, `wai.live.e2ee`, - `wai.live.watermark` and `wai.live.ads`; timed text, `wai.text.timed`). - It names no payload format. + `wai.live.watermark` and `wai.live.ads`; timed text, `wai.text.timed`; AI usage + preferences and consumption claims, `wai.ai.usage`). It names no payload format. A derivation or record string is not a payload capability, so it is not dispatchable (SPEC §4): dispatch never takes an envelope's capability that names @@ -65,8 +65,8 @@ bindings ([http-delivery](extensions/http-delivery.md), they serve ([jwp-receipts](extensions/jwp-receipts.md) §7), [packaging](extensions/packaging.md), [live-events](extensions/live-events.md), [live-rooms](extensions/live-rooms.md), [capture-binding](extensions/capture-binding.md), -[live-compose](extensions/live-compose.md), [live-recording](extensions/live-recording.md) -and [timed-text](extensions/timed-text.md) put on the wire outside a +[live-compose](extensions/live-compose.md), [live-recording](extensions/live-recording.md), +[timed-text](extensions/timed-text.md) and [ai-usage](extensions/ai-usage.md) put on the wire outside a manifest — magic numbers, a media type, `codecs` prefixes, paths, playlist tags and attributes, document kinds, catalog fields, MOQT Setup Options and HTTP fields — each with its kind, the section that defines it, and whether it is defined, provisional (test material only until @@ -157,6 +157,7 @@ and a role are fixed once registered. | string | kind | status | media | role | licence | replicate | sets | registered in | |---|---|---|---|---|---|---|---|---| +| `wai.ai.usage` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content | | `wai.asset.provenance` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content | | `wai.audio.aac` | payload | registered | audio | standalone | patent-pool | standard-unclassified | — | §5 Broadcast and streaming codecs | | `wai.audio.ac4` | payload | registered | audio | standalone | proprietary | standard-unclassified | — | §5 Immersive audio, cinema, volumetric, haptics, HDR | @@ -305,11 +306,13 @@ and a role are fixed once registered. | `WAB1` | magic | defined | live-ads §4.2 | | `WAI-Room-Challenge` | http-header | proposed | live-rooms §10.5.3 | | `WAI-Room-Proof` | http-header | proposed | live-rooms §10.5.3 | +| `WAI-Usage-Claim` | http-header | proposed | ai-usage §7.3 | | `WAI1` | magic | defined | SPEC §2 | | `WAI2` | magic | defined | SPEC §7.1 | | `WAI_INNER_DIGEST` | catalog | defined | confidentiality §4.4 | | `WAI_RECEIPT` | catalog | defined | moq-streaming-format §5 | | `WAP1` | magic | defined | live-ads §3.1 | +| `WAU1` | magic | defined | ai-usage §4.2 | | `WCD1` | magic | defined | live-compose §3.1 | | `WCL1` | magic | defined | live-recording §5.1 | | `WCN1` | magic | defined | capture-binding §8.1 | @@ -366,6 +369,7 @@ and a role are fixed once registered. | `wai2` | codecs-prefix | defined | http-delivery §4.1 | | `wai2` | fourcc | provisional | http-delivery Appendix B | | `waiC` | fourcc | provisional | http-delivery Appendix B | +| `wai_ai_usage` | catalog | defined | moq-streaming-format §4 | | `wai_capabilities` | catalog | defined | moq-streaming-format §4 | | `wai_delivery_timeout_ms` | catalog | defined | packaging §8 | | `wai_determinism` | catalog | defined | moq-streaming-format §4 | diff --git a/wai/SPEC.md b/wai/SPEC.md index 5eaeabb59..494c6970c 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -594,8 +594,8 @@ kind: `wai.video.package` and `wai.video.channel`; and the live records `wai.live.events`, `wai.live.room`, `wai.capture.take`, `wai.live.compose`, `wai.live.recording`, `wai.live.metering`, `wai.live.steering`, `wai.live.moderation`, `wai.live.e2ee`, - `wai.live.watermark` and `wai.live.ads`, and timed text, `wai.text.timed`), which names no - payload format. + `wai.live.watermark` and `wai.live.ads`, timed text, `wai.text.timed`, and AI usage + preferences and consumption claims, `wai.ai.usage`), which names no payload format. Only payload, companion and refinement capabilities have a media class, a fidelity tier, a replicate class and a role. A derivation or record string is not a payload @@ -1071,6 +1071,25 @@ recomputes the fill and the asset list, refuses a second receipt of one session and holds the spend of every receipt together to the decider grant's ceiling: [`extensions/live-ads.md`](extensions/live-ads.md). +And for **AI usage preferences** and the agents that consume media — `wai.ai.usage`: a usage +statement carries, verbatim and each in its own member, the AI usage preferences of +`draft-ietf-aipref-vocab-08` in its Structured Field form, the TDM Reservation Protocol's +`tdm-reservation` and `tdm-policy`, and the entries of a `cawg.training-mining` assertion +(Training and Data Mining Assertion 1.1), with no category of WAI's own. The manifest member +`ai_usage` carries it inside the envelope, where the object's receipt signs it with the content, +so an object whose statement is removed or altered fails its receipt (`usage_unbound`); a room +usage record, `WAU1`, signed by the room authority and chained by the instant it takes effect, +carries a room's; and a grant's attribute `ai_usage` carries the terms one principal is granted. +An origin writes `draft-ietf-aipref-attach-05`'s `Content-Usage` field and the TDMRep fields from +the signed statement, a MoQ catalog states it as `wai_ai_usage`, and a field or catalog that +contradicts the signed statement is refused. One rule decides a use: a grant's terms, then the +signed statements, the most restrictive governing, then unsigned statements where nothing is +signed. An agent signs a consumption claim, the jwp-receipts class `consumption`, under its +`did:key`: the objects by content hash, the term it acted under in each vocabulary, the +statements it saw and its grant; a verifier checks it against the objects, their statements and +the room, and refuses a use they disallow (`usage_disallowed`), and a relay or gateway MAY require +claims before it serves an agent: [`extensions/ai-usage.md`](extensions/ai-usage.md). + ### Payloads of the WAI-native integer codecs This revision pins a payload format to each WAI-native integer codec whose row named diff --git a/wai/claim-conformance/claim_verify.py b/wai/claim-conformance/claim_verify.py index d50987c24..0e7093178 100644 --- a/wai/claim-conformance/claim_verify.py +++ b/wai/claim-conformance/claim_verify.py @@ -20,6 +20,9 @@ claim are read, verified and their figures read by the OER reference verifier python3 claim_verify.py verdicts print `nameverdict` for every vector of a corpus-format file, ignoring any declared outcome (the input to differential.py) + python3 claim_verify.py consumption + steps 1-4 over the `consumption` claims of the + AI usage corpus (live-conformance/usage) Exit status 0 only if every vector reports its declared outcome, and the fixture and the bracket arithmetic this script computes equal the literals pinned in wai-rs. @@ -172,7 +175,8 @@ ENVELOPE = ["kind", "class", "subject_type", "subject", "seq", "issued_at", "bod ADMITS = {"energy": {"object"}, "lawful-basis": {"object", "receipt"}, "log-inclusion": {"receipt"}, "soft-binding": {"object"}, "binding-recovery": {"object"}, "measurement": {"object", "receipt"}, "operating-point": {"object", "receipt"}, - "stage": {"object"}, "delivery-gap": {"object"}, "reconstruct-session": {"object"}} + "stage": {"object"}, "delivery-gap": {"object"}, "reconstruct-session": {"object"}, + "consumption": {"object"}} FIGURE = ["figure_profile", "figure_index", "group", "joules_micro", "energy_provenance", "energy_uncertainty", "energy_counter_filtering", "energy_calibration_ref"] BODY = { @@ -186,6 +190,8 @@ BODY = { "stage": ["vocabulary", "op_requested", "op_committed", "op_delivered", "presented", "units_by_stage", "tier", "stage_hash", "objects", "conflicts", "deadline_misses", "bytes_envelopes"], "delivery-gap": ["track_alias", "group_id", "object_id", "reason", "observed_at"], + # §2.4.13: ai-usage §6.1. + "consumption": ["objects", "usage", "seen", "grant"], "reconstruct-session": ["session_nonce", "media", "host", "decoders", "units", "leaf_count", "output_root", "primary_units", "fallback_units", "switches", "switches_total", "deadline_us", "missed", "missed_units", "stage_sets", "framing", "counted_primary", "counted_fallback", @@ -415,6 +421,19 @@ def check_body(cls, b): need(o["reason"] is None or isinstance(o["reason"], str), 2) need(all(is_int(b[k]) for k in ("conflicts", "deadline_misses", "bytes_envelopes")), 2) return + if cls == "consumption": + # ai-usage §6.1: the arrays' lengths and a term's are in the type column. + need(isinstance(b["objects"], list) and 1 <= len(b["objects"]) <= 1024 + and all(is_hex(h, 32) for h in b["objects"]), 2) + need(isinstance(b["usage"], list) and 1 <= len(b["usage"]) <= 3, 2) + for e in b["usage"]: + need(isinstance(e, dict), 2) + members(e, ["vocabulary", "term"], 2) + need(e["vocabulary"] in CONSUMPTION_VOCABULARIES, 2) + need(e["term"] is None or (isinstance(e["term"], str) and 1 <= len(e["term"].encode()) <= 255), 2) + need(isinstance(b["seen"], list) and len(b["seen"]) <= 64 and all(is_hex(h, 32) for h in b["seen"]), 2) + need(b["grant"] is None or is_hex(b["grant"], 32), 2) + return if cls == "delivery-gap": need(all(is_int(b[k]) for k in ("track_alias", "group_id", "object_id")), 2) need(b["reason"] in GAP_REASONS, 2) @@ -1398,6 +1417,8 @@ def class_rules(c): stage_rules(b) elif cls == "reconstruct-session": session_rules(b) + elif cls == "consumption": + consumption_rules(c) elif cls == "log-inclusion": need(b["log_receipt"] != "", 4, "log_inclusion_no_evidence") elif cls in ("soft-binding", "binding-recovery"): @@ -1409,6 +1430,45 @@ def class_rules(c): need(b["observed"] != c["subject"], 4, "recovery_of_hard_bound_object") +# §2.4.13: ai-usage §3, §6.2. The vocabularies, and the terms each defines. +CONSUMPTION_VOCABULARIES = ("aipref", "cawg", "tdmrep") +AIPREF_LABELS = ("train-ai", "ai-use", "search") # draft-ietf-aipref-vocab-08 §6.1 +CAWG_KEYS = ("cawg.data_mining", "cawg.ai_inference", "cawg.ai_generative_training", "cawg.ai_training") +B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + + +def did_key(pk): + """§7.1's Ed25519 did:key: `did:key:z` and base58btc of 0xed 0x01 and the key.""" + b = b"\xed\x01" + pk + n, out = int.from_bytes(b, "big"), "" + while n: + n, r = divmod(n, 58) + out = B58[r] + out + return "did:key:z" + "1" * (len(b) - len(b.lstrip(b"\x00"))) + out + + +def cawg_key(k): + """ai-usage §3.4: components of letters, digits, `_` and `-`, separated by single dots.""" + parts = k.split(".") + return 1 <= len(k.encode()) <= 255 and all(p and re.fullmatch(r"[A-Za-z0-9_-]+", p) for p in parts) + + +def consumption_rules(c): + """ai-usage §6.2, in its order.""" + b = c["body"] + asc = lambda xs: all(x < y for x, y in zip(xs, xs[1:])) # noqa: E731 + need(asc(b["objects"]), 4, "consumption_objects_unordered") + need(c["subject"] == b["objects"][0], 4, "consumption_subject_mismatch") + need(asc([e["vocabulary"] for e in b["usage"]]), 4, "consumption_usage_unordered") + for e in b["usage"]: + t, v = e["term"], e["vocabulary"] + known = t is None or (t in AIPREF_LABELS if v == "aipref" else t == "tdm-reservation" if v == "tdmrep" + else t in CAWG_KEYS or (cawg_key(t) and not t.startswith("cawg."))) + need(known, 4, "consumption_term_unknown") + need(asc(b["seen"]), 4, "consumption_seen_unordered") + need(c["signer_id"] == did_key(bytes.fromhex(c["signer_pubkey"])), 4, "consumption_signer_not_principal") + + def stage_layers(pt): """The layers a stage point names: exactly the member `wai-stage:layers`, 0 to 255.""" if len(pt) != 1 or "wai-stage:layers" not in pt or pt["wai-stage:layers"] > 255: @@ -1938,6 +1998,27 @@ def brackets_match_wai_rs(): return fails +def consumption_claims(path): + """§3.1 steps 1-4 over the `consumption` claims of the AI usage corpus + (live-conformance/usage/vectors.json, ai-usage §10): a claim stated refused at one of those + steps is refused there with the stated reason, and every other claim passes them.""" + with open(path, encoding="utf-8") as f: + doc = json.load(f) + fail = 0 + for c in doc["claims"]: + o = c["outcome"] + want = (o["step"], o["reason"]) if o["outcome"] == "refuse" and o["step"] <= 4 else None + try: + verify_claim(c["wire"]) + got = None + except Reject as r: + got = (r.step, r.reason) + fail += got != want + print(f"{'PASS' if got == want else 'FAIL'} {c['name']} expected={want} got={got}") + print(f"{len(doc['claims'])} consumption claims, {fail} failure(s)") + return 1 if fail else 0 + + def main(argv): if argv[1:2] == ["brackets"]: for name, br, _, _ in bracket_fixture(): @@ -1946,6 +2027,8 @@ def main(argv): return 0 if argv[1:2] == ["verdicts"] and len(argv) == 3: return verdicts(argv[2]) + if argv[1:2] == ["consumption"] and len(argv) == 3: + return consumption_claims(argv[2]) if argv[1:2] == ["fixture"]: c, canon, link = fixture() print("sig ", c["sig"]) diff --git a/wai/extensions/ai-usage.md b/wai/extensions/ai-usage.md index 043e36806..db82e2882 100644 --- a/wai/extensions/ai-usage.md +++ b/wai/extensions/ai-usage.md @@ -338,15 +338,15 @@ that traces the agent. Its body has exactly these members, read in this order at | member | type | meaning | |---|---|---| -| `objects` | array of 32 B hex | the content hashes of the objects consumed, 1 to 1024 | -| `usage` | array of objects | the use, in each vocabulary the agent answers, 1 to 3 entries | -| `seen` | array of 32 B hex | the digests of the statements that governed the objects (§5.1), 0 to 64 | +| `objects` | array of 1 to 1024 32 B hex | the content hashes of the objects consumed | +| `usage` | array of 1 to 3 entries | the use, in each vocabulary the agent answers | +| `seen` | array of 0 to 64 32 B hex | the digests of the statements that governed the objects (§5.1) | | `grant` | 32 B hex or `null` | the digest of the room grant the agent consumed under (live-rooms §4.2) | A `usage` entry has exactly `vocabulary`, one of `aipref`, `cawg` and `tdmrep`, and `term`, a -string of 1 to 255 bytes or `null`. A term names the use as that vocabulary names it; `null` -states that the use falls under no term of the vocabulary. A value of `vocabulary` outside the -three is `field_type`. +string of 1 to 255 bytes or `null`, read in that order. A term names the use as that vocabulary +names it; `null` states that the use falls under no term of the vocabulary. An array of another +length, a `vocabulary` outside the three and a `term` of another length are `field_type`. ### 6.2 Class rules diff --git a/wai/extensions/http-delivery.md b/wai/extensions/http-delivery.md index 37c7adea3..6d87332e3 100644 --- a/wai/extensions/http-delivery.md +++ b/wai/extensions/http-delivery.md @@ -248,6 +248,22 @@ refinement sits, depend on the media class. | binary (parameter sets, circuits, ternary) | a parameter set served as an immutable resource, verified by its pin (§8.5) | — | — | | records and derivations | carried as objects, never named in `codecs` (`codecs_not_media`) | — | — | +### 5.6 AI usage preferences + +An object whose manifest carries a usage statement ([ai-usage](ai-usage.md) §4.1) carries it +signed, with the content. An origin **SHOULD** also send it in the fields +`draft-ietf-aipref-attach-05` §2 and the TDM Reservation Protocol §6.2 define, written from the +signed statement (ai-usage §4.4): `Content-Usage` with the `aipref` value, byte for byte, and +`tdm-reservation` and, where the statement has one, `tdm-policy`. It names them in +`Access-Control-Expose-Headers` beside §5.3's, so that a client in a browser can read them. +These fields are unsigned: a client compares them with the signed statement, and a response +whose fields contradict it is refused (`header_conflicts`), the signed statement governing. + +A gateway that requires consumption claims of agents (ai-usage §7) reads the request field +`WAI-Usage-Claim`, names it in `Access-Control-Allow-Headers` beside §5.3's, and answers a +request it refuses with `403`. It never serves other bytes under an object's name: refusing is +not selecting (§5.3). + ## 6. The head proof ### 6.1 What it proves diff --git a/wai/extensions/jwp-receipts.md b/wai/extensions/jwp-receipts.md index b51584496..77e50fafb 100644 --- a/wai/extensions/jwp-receipts.md +++ b/wai/extensions/jwp-receipts.md @@ -38,7 +38,8 @@ In scope: class. `group` and `object` receipts are the **delivery** class; a `claim` receipt (§2.4.3) carries one of the others — `energy`, `lawful-basis`, `log-inclusion`, `measurement`, `operating-point`, - `stage`, `delivery-gap`, `reconstruct-session` (§2.4.5–§2.4.12) — on one + `stage`, `delivery-gap`, `reconstruct-session`, `consumption` + (§2.4.5–§2.4.13) — on one envelope, one canonical form and one signature rule, so a statement other than delivery does not need a field set of its own. - **Objects not delivered** (§3, §4.4): an object a subscriber did not @@ -396,6 +397,7 @@ key. | `stage` | `claim` | `object` (the stage set's base) | §2.4.10 | the party that observed the stage set | | `delivery-gap` | `claim` | `object` (the object not forwarded) | §2.4.11 | the relay that did not forward it | | `reconstruct-session` | `claim` | `object` (the session's entry object) | §2.4.12 | the sink that reconstructed: the key its decoder host holds | +| `consumption` | `claim` | `object` (the first object it names) | §2.4.13 | the agent that consumed the objects: its key, named by its `did:key` | Rules: @@ -1273,6 +1275,15 @@ one of this session. The `claim` an `end` observation carries ([delivery-telemetry](delivery-telemetry.md) §2, its §2.1) is this claim's `link_hash`. +#### 2.4.13. `class: "consumption"` + +An agent's statement that it consumed objects, for a use it names in the AI usage +vocabularies a usage statement carries, having seen the statements that governed them and +holding the room grant it names. [ai-usage](ai-usage.md) defines it: its body's members are +§6.1 there (`objects`, `usage`, `seen` and `grant`), read at §3.1 step 2; its class rules are +§6.2 there, at step 4; and its context is §6.3 there, at step 6. Its `subject` is the first of +`objects`, and its `signer_id` is the `did:key` of its `signer_pubkey`. + ### 2.5. Worked example Manifest JSON (group receipt, formatted for readability — wire form @@ -1913,7 +1924,8 @@ order listed. is not 64 of them; a value outside an enumeration this extension defines here (`record`, `binding_type`, a bracket's `opening`, a build's `profile`, a stage's `tier`, a stage object's `status` and `evidence`, a - gap's `reason`, a session's `framing`, its host's `profile`, and the paths + consumption claim's usage `vocabulary`, a gap's `reason`, a session's + `framing`, its host's `profile`, and the paths of its decoder entries and switches); a date-time that is not one of §2.4.3 (a gap's `observed_at`); a URI that is not absolute; a term that is not one of §2.4.3 (empty, longer than 2048 bytes, or @@ -1945,7 +1957,9 @@ order listed. agree; and, in a `reconstruct-session` claim, how its counts agree, a decoder entry's grammar and tier, the switches' reasons and order, the missed presentations, the order of its stage sets, its counted bytes and - its classical rendition. + its classical rendition; and, in a `consumption` claim, the order of its + objects, usage entries and seen digests, its subject, its terms and its + `signer_id`. 3. **Signature.** Ed25519 over §2.4.3's signing input, verified by the rule every WAI signature is held to (SPEC §7 *Signatures*): a canonical key not of small order, `S` < L, `R` not of small order, and the @@ -1986,7 +2000,9 @@ order listed. verified by inclusion at least (§2.4.11); a `reconstruct-session` claim against its outputs, the objects it received, the `WAI2` envelopes it received and the `stage` claims it names, in that order, each that the verifier holds, the - `stage` claims verified under steps 1–4 (§2.4.12). A + `stage` claims verified under steps 1–4 (§2.4.12); a `consumption` claim against + the objects it names, with their receipts, the usage statements that govern them + and its room, as [ai-usage](ai-usage.md) §6.3 orders them (§2.4.13). A declaration is verified here under steps 1–4, and one that fails them — or is not JSON — is `context_invalid`: its members, `signer_pubkey` among them, are its signer's only once its signature @@ -2066,6 +2082,12 @@ and never folds it in: confirmation changes the outcome of no step here. | 4 | `session_stage_sets_invalid` | `stage_sets` out of order, or an entry twice | | 4 | `session_counted_invalid` | `counted_discarded` above `counted_primary` and `counted_fallback` together | | 4 | `session_classical_invalid` | `classical_source` present without `classical_declared` or absent with it, or not `wai2-table` | +| 4 | `consumption_objects_unordered` | a `consumption` claim's `objects` are not in strictly ascending order ([ai-usage](ai-usage.md) §6.2) | +| 4 | `consumption_subject_mismatch` | its `subject` is not the first of its `objects` | +| 4 | `consumption_usage_unordered` | its `usage` entries are not in strictly ascending order of `vocabulary` | +| 4 | `consumption_term_unknown` | a `term` is not a term of its vocabulary | +| 4 | `consumption_seen_unordered` | its `seen` digests are not in strictly ascending order | +| 4 | `consumption_signer_not_principal` | its `signer_id` is not the `did:key` of its `signer_pubkey` | | 6 | `energy_disagrees_with_delivery` | the figure differs from the `object` receipt's | | 6 | `energy_class_disagrees_with_delivery` | the object's leaf is labelled, and the `energy` claim's class or evidence differs from the label | | 6 | `figure_disagrees` | the receipt a `measurement` or `operating-point` claim names carries another figure at `figure_index` | @@ -2099,6 +2121,11 @@ and never folds it in: confirmation changes the outcome of no step here. | 6 | `session_layers_mismatch` | a presentation through a decoder entry whose envelope is held is at another layer count than that envelope gives | | 6 | `session_unit_outside_object` | a presentation through a decoder entry whose envelope is held is of a unit that envelope does not cover, from its `first_unit` | | 6 | `session_classical_mismatch` | the envelopes' classical renditions the session did not decode give another `classical_declared` | +| 6 | `consumption_object_unknown` | a `consumption` claim names an object the verifier does not hold ([ai-usage](ai-usage.md) §6.3) | +| 6 | `grant_mismatch` | the grant it names is not the signer's, not good at an object's time, or names objects that are not the room's | +| 6 | `preference_mismatch` | its `seen` is not the digests of the statements that govern its objects | +| 6 | `usage_unanswered` | its `usage` does not answer a vocabulary a governing statement carries | +| 6 | `usage_disallowed` | a use it names is disallowed for one of its objects | | 6 | `session_stage_disagrees` | a `stage` claim the session names is about a base, or lists an arrived layer object, the session did not receive; counts a unit the session never presented, or more units at a stage than the session presented at it or above; or no claim whose set holds a unit lists an applied layer that reaches a stage the session presented it at | ### 3.2. Recovery through a soft binding, and the ancestor rule @@ -3242,11 +3269,11 @@ A confirmation needs every one of them: | item | authorities | |---|---| | a `group` receipt, and the `object` receipts under it | `delivery`; and `energy/` for each acquisition class an `object` receipt under it labels, where its leaf binds a label (§2.2), and for the class a labelled group payload signs for the group's total (§2.3) | -| a `claim` | its class's authority: `energy/` followed by its `energy_provenance` for `energy`, for example `energy/OnChipCounter`; `lawful-basis/declaration` or `lawful-basis/decision` by its `record`; its class name for every other class — `log-inclusion`, `soft-binding`, `binding-recovery`, `measurement`, `operating-point`, `stage`, `delivery-gap` and `reconstruct-session`. And `energy/` followed by its `energy_provenance` whenever its body carries that member, whatever its class | +| a `claim` | its class's authority: `energy/` followed by its `energy_provenance` for `energy`, for example `energy/OnChipCounter`; `lawful-basis/declaration` or `lawful-basis/decision` by its `record`; its class name for every other class — `log-inclusion`, `soft-binding`, `binding-recovery`, `measurement`, `operating-point`, `stage`, `delivery-gap`, `reconstruct-session` and `consumption`. And `energy/` followed by its `energy_provenance` whenever its body carries that member, whatever its class | | the energy figure of a `reconstruct-session` claim whose `energy` is not `null` | `reconstruct-session`, and `energy/` followed by `energy.energy_provenance` | | a receipt of another WAI family that signs its own `signer_pubkey` and `signer_id` | `receipt`; and `energy/` for each class its figures carry a label for | | a module grant (§7.9) | `module/decoder` or `module/reconstruct`, by its `form` | -| a room descriptor or room grant ([live-rooms](live-rooms.md) §3, §4) | `live/room` | +| a room descriptor or room grant ([live-rooms](live-rooms.md) §3, §4), or a room usage record ([ai-usage](ai-usage.md) §4.2) | `live/room` | | an upload receipt of a take ([capture-binding](capture-binding.md) §6) | `capture/store` | | a per-hop session record ([live-metering](live-metering.md) §3) | `live/relay`, `live/compositor` or `live/ingest`, by its role; and `energy/` for its figure's class where it is metered | | a room-session receipt (live-metering §4) signed by a key other than the room authority | `live/meter`; and, from any signer, `energy/` for each class it totals | diff --git a/wai/extensions/live-rooms.md b/wai/extensions/live-rooms.md index af4699bdb..101cb2c69 100644 --- a/wai/extensions/live-rooms.md +++ b/wai/extensions/live-rooms.md @@ -190,6 +190,17 @@ policy's fields and their meaning; the review workflow and its verification are `mode` is at most 2, and `commit_interval_ms` and `jitter_ms` keep the rule above. +### 3.8 AI usage preferences + +The descriptor's layout holds no AI usage preference. A room states one in a room usage +record, `WAU1` ([ai-usage](ai-usage.md) §4.2): a usage statement the authority signs, naming +the room id, chained so that a preference can change at an instant of the room clock and never +for what was published before it. An object of the room is governed by the record in effect at +its time, and by its own statement where its manifest carries one (ai-usage §5.1). A grant +states the terms its principal is granted in the attribute `ai_usage` (§4.4), and a principal +whose grant carries terms is an agent principal, which a relay MAY require consumption claims +of before it serves it (ai-usage §7). + ## 4. The room grant ### 4.1 Layout @@ -273,8 +284,11 @@ composition descriptor names, and gives each its syntax and its meaning (live-co §4.3); this revision checks only the form of the entries. Capture binding ([capture-binding](capture-binding.md) §3) reads two more: `subject`, the principal a device captures for, as 64 lowercase hexadecimal digits of its key; and `minor` (`true` -or `false`), whether the authority marks that principal a minor. Attributes are signed with the grant, -so a layout computed from them is computed from what the authority stated. +or `false`), whether the authority marks that principal a minor. AI usage +([ai-usage](ai-usage.md) §4.3) reads `ai_usage`: the canonical form of a usage statement, the +terms the authority grants the principal, which override the room's preference for the uses +they name. Attributes are signed with the grant, so a layout computed from them, and the terms, +are what the authority stated. ### 4.5 How a receipt names a grant diff --git a/wai/extensions/moq-streaming-format.md b/wai/extensions/moq-streaming-format.md index 6aac407cb..addab7725 100644 --- a/wai/extensions/moq-streaming-format.md +++ b/wai/extensions/moq-streaming-format.md @@ -80,6 +80,7 @@ fields, the capabilities a sink needs to dispatch what the track carries: | `wai_receipts` | boolean | Whether objects carry receipts per §5. | | `wai_receipt_alias` | integer | The `track_alias` the publisher signs in this track's receipts ([jwp-receipts](jwp-receipts.md) §2.4.1), from 0 to 2^53 − 1. | | `wai_receipts_track` | string | The name, in this track's namespace, of the track that carries this track's receipts (§4.1). | +| `wai_ai_usage` | object | The usage statement of the track's objects ([ai-usage](ai-usage.md) §3), or of the room record in effect for a room's track. Absent means the catalog states none. | - `wai_receipt_alias` names the track's receipt chain, which a verifier keeps per publisher key and `track_alias` (jwp-receipts §3 step 3). MOQT's own Track Alias is @@ -102,6 +103,10 @@ fields, the capabilities a sink needs to dispatch what the track carries: - `wai_determinism` is the **weakest** tier on the track, not the strongest. Declaring the strongest would let a track advertise a guarantee most of its objects do not meet. +- `wai_ai_usage` lets an agent know a track's AI usage preference before it + subscribes. The catalog is unsigned and the objects' own statements are + signed with them: a catalog whose statement differs from an object's is refused + (`catalog_conflicts`), and the object's governs (ai-usage §4.5, §5.3). These are custom catalog fields under MSF's extension rule (MSF §5: custom field names "MUST NOT collide" with MSF's, and a parser ignores a field it does not diff --git a/wai/live-conformance/README.md b/wai/live-conformance/README.md index eff884d3d..88ec5f23a 100644 --- a/wai/live-conformance/README.md +++ b/wai/live-conformance/README.md @@ -17,6 +17,7 @@ and the independent verifier of each, beside it. | [`ads/`](ads/) | [live-ads](../extensions/live-ads.md) (`wai.live.ads`) | `live_ads_verify.py` | | [`e2ee/`](e2ee/) | [confidentiality](../extensions/confidentiality.md) (`wai.live.e2ee`) | `e2ee_verify.py` | | [`watermark/`](watermark/) | [forensic-watermark](../extensions/forensic-watermark.md) (`wai.live.watermark`) | `live_watermark_verify.py` | +| [`usage/`](usage/) | [ai-usage](../extensions/ai-usage.md) (`wai.ai.usage`) | `ai_usage_verify.py` | **Provenance.** Generated here: `wai_live_events_vectors gen` writes `events/`, `wai_live_room_vectors gen` writes `rooms/`, `wai_capture_take_vectors gen` writes @@ -24,11 +25,11 @@ and the independent verifier of each, beside it. writes `recordings/`, `wai_timed_text_vectors gen` writes `captions/` and `wai_live_metering_vectors gen` writes `metering/`, `wai_energy_steering_vectors gen` writes `steering/`, `wai_live_moderation_vectors gen` writes `moderation/`, -`wai_live_watermark_vectors gen` writes `watermark/` and `wai_live_ads_vectors gen` writes -`ads/`, each from records authored in code, from fixed seeds (`events/README.md`, -`rooms/README.md`, `takes/README.md`, `compose/README.md`, `recordings/README.md`, -`captions/README.md`, `metering/README.md`, `steering/README.md`, `moderation/README.md`, -`watermark/README.md`, `ads/README.md`). `joule_moq_e2ee_vectors gen` (in +`wai_live_watermark_vectors gen` writes `watermark/`, `wai_live_ads_vectors gen` writes +`ads/` and `wai_ai_usage_vectors gen` writes `usage/`, each from records authored in code, from +fixed seeds (`events/README.md`, `rooms/README.md`, `takes/README.md`, `compose/README.md`, +`recordings/README.md`, `captions/README.md`, `metering/README.md`, `steering/README.md`, +`moderation/README.md`, `watermark/README.md`, `ads/README.md`, `usage/README.md`). `joule_moq_e2ee_vectors gen` (in `joule-code/joule-code-rs/crates/joule-moq-mls`) writes `e2ee/vectors.json` from fixed seeds, and `e2ee/` also holds the test vectors RFC 9605 and `draft-ietf-moq-secure-objects-01` publish, as printed (`e2ee/README.md`). One recorded record: `metering/relay-record.wmh` and @@ -51,4 +52,5 @@ python3 live_moderation_verify.py moderation python3 live_ads_verify.py ads python3 e2ee_verify.py e2ee python3 live_watermark_verify.py watermark +python3 ai_usage_verify.py usage ``` diff --git a/wai/live-conformance/ai_usage_verify.py b/wai/live-conformance/ai_usage_verify.py new file mode 100644 index 000000000..fcb41bdcd --- /dev/null +++ b/wai/live-conformance/ai_usage_verify.py @@ -0,0 +1,1228 @@ +#!/usr/bin/env python3 +"""Independent verifier of the AI usage corpus (extensions/ai-usage.md). + +Written from the extension's text. It shares no code with wai-rs, which generates the corpus, +and never runs it. It checks Ed25519 signatures with ../claim-conformance/_ed25519.py, WAI's +strict rule (SPEC §7 *Signatures*) written from RFC 8032 with the standard library, and +computes BLAKE3 with ../claim-conformance/_blake3.py, written from its specification. A room's +descriptor and grants are read by the live-rooms corpus's own second implementation, +live_rooms_verify.py (live-rooms §3, §4), written from its text alone. Everything else is here: + +- RFC 9651's Dictionary parser, as §3.3 reads it, and the vocabularies' preferences (§3); +- the canonical form (RFC 8785 over the subset jwp-receipts §2.4.3 allows) and the digest; +- the envelope's binding (§4.1): the content hash, the group receipt's legacy payload and its + signature, the object's inclusion proof (jwp-receipts §2.1-§2.3), the manifest's member; +- the room usage record (§4.2) and a grant's terms (§4.3); +- the HTTP fields (§4.4) and the catalog field (§4.5); +- the precedence rule (§5); +- the consumption claim (§6): jwp-receipts §3.1 steps 1 to 5 for its class, then §6.3; +- the gate (§7). + +For every case it requires the outcome the corpus states; of the corpus, that every reason of +§9 appears in it and that no other file is there. Then it mutates copies of the corpus, one +defect each, and requires every copy to fail; and it drops each of its own checks in turn and +requires the corpus to catch the weakened verifier. + + python3 ai_usage_verify.py + +Exit status 0 only if every case verifies, every mutated copy fails and every weakened +verifier fails. Python 3.8 or later; standard library only. +""" + +import copy +import hashlib +import json +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) +sys.path.insert(0, os.path.join(HERE, "..", "claim-conformance")) +import live_events_verify as LEV # noqa: E402 +import live_rooms_verify as LRV # noqa: E402 +import _ed25519 as ED # noqa: E402 +import _blake3 as B3 # noqa: E402 + +EXTENSION = os.path.join(HERE, "..", "extensions", "ai-usage.md") + +AIPREF_REV = "draft-ietf-aipref-vocab-08" # §3.2 +CAWG_REV = "1.1" # §3.4 +TDMREP_REV = "CG-FINAL-tdmrep-20240510" # §3.5 +LABELS = ("train-ai", "ai-use", "search") # draft-ietf-aipref-vocab-08 §6.1 +CAWG_KEYS = ("cawg.data_mining", "cawg.ai_inference", "cawg.ai_generative_training", "cawg.ai_training") +TDM_TERM = "tdm-reservation" +VOCABULARIES = ("aipref", "cawg", "tdmrep") +ROOM_MAGIC = b"WAU1" +ROOM_DOMAIN = b"wai:ai-usage-room\x01" # §4.2 +OBJECT_DOMAIN = b"moq-jwp:object\x01" # jwp-receipts §2.1 +LEAF, NODE = b"moq-jwp:merkle-leaf\x01", b"moq-jwp:merkle-node\x01" +GROUP_SIGN = b"moq-jwp:group-sign\x01" # jwp-receipts §2.3 +CLAIM_SIGN, CLAIM_LINK = b"moq-jwp:claim\x01", b"moq-jwp:claim-link\x01" +MAX_INT = 2 ** 53 - 1 + +# A check named here is left out: the verifier weakened on purpose, to show the corpus catches it. +SKIP = set() + + +class Refuse(Exception): + def __init__(self, reason, step=None): + super().__init__(reason) + self.reason, self.step = reason, step + + +def sha(*parts): + h = hashlib.sha256() + for p in parts: + h.update(p) + return h.digest() + + +def b3(b): + return B3.blake3(b) + + +def reasons_of_extension(): + """§9's codes, read from the text.""" + with open(EXTENSION, encoding="utf-8") as f: + text = f.read() + at = text.index("\n## 9. Reasons") + end = text.index("\n## ", at + 1) + return [line.split("`")[1] for line in text[at:end].splitlines() if line.startswith("| `")] + + +REASONS = reasons_of_extension() + + +# ---------------------------------------------------------------- JSON and the canonical form + +class Number: + """A JSON number that is not an unsigned integer: never a value a statement or claim holds.""" + + +def strict_json(text): + """JSON in which no object names a member twice (§3.6); a number that is not a plain + unsigned integer is kept as a Number marker. ValueError when it does not read.""" + def pairs(ps): + d = {} + for k, v in ps: + if k in d: + raise ValueError("repeated member " + k) + d[k] = v + return d + + def no_constant(c): + raise ValueError(c) + + v = json.loads(text, object_pairs_hook=pairs, parse_constant=no_constant, + parse_float=lambda s: Number(), parse_int=lambda s: Number() if s.startswith("-") else int(s)) + + def walk(x, level): + # jwp-receipts §3.1 step 1: no escape that leaves a surrogate unpaired, at most 32 levels. + if isinstance(x, str): + if any("\ud800" <= c <= "\udfff" for c in x): + raise ValueError("an unpaired surrogate") + elif isinstance(x, (list, dict)): + if level + 1 > 32: + raise ValueError("nested too deeply") + for k in (x if isinstance(x, dict) else []): + walk(k, level) + for y in (x.values() if isinstance(x, dict) else x): + walk(y, level + 1) + walk(v, 0) + return v + + +def is_uint(v): + return isinstance(v, int) and not isinstance(v, bool) and 0 <= v <= MAX_INT + + +def jcs(v): + """RFC 8785 over strings, unsigned integers, null, booleans, arrays and objects.""" + if v is None: + return "null" + if v is True or v is False: + return "true" if v else "false" + if isinstance(v, int) and not isinstance(v, Number): + if not 0 <= v <= MAX_INT: + raise ValueError("an integer outside the subset") + return str(v) + if isinstance(v, str): + return json.dumps(v, ensure_ascii=False) + if isinstance(v, list): + return "[" + ",".join(jcs(x) for x in v) + "]" + if isinstance(v, dict): + keys = sorted(v, key=lambda k: k.encode("utf-16-be")) + return "{" + ",".join(json.dumps(k, ensure_ascii=False) + ":" + jcs(v[k]) for k in keys) + "}" + raise ValueError("a value outside the subset") + + +# ---------------------------------------------------------------- §3.3 RFC 9651 Dictionary + +class SFFail(Exception): + pass + + +LCALPHA = set("abcdefghijklmnopqrstuvwxyz") +DIGIT = set("0123456789") +ALPHA = LCALPHA | set("ABCDEFGHIJKLMNOPQRSTUVWXYZ") +TCHAR = ALPHA | DIGIT | set("!#$%&'*+-.^_`|~") +B64 = ALPHA | DIGIT | set("+/") + + +class SF: + def __init__(self, s): + self.s, self.i = s, 0 + + def peek(self): + return self.s[self.i] if self.i < len(self.s) else None + + def take(self): + c = self.peek() + if c is None: + raise SFFail() + self.i += 1 + return c + + def need(self, c): + if self.peek() != c: + raise SFFail() + self.i += 1 + + def spaces(self): + while self.peek() == " ": + self.i += 1 + + def ows(self): + while self.peek() in (" ", "\t"): + self.i += 1 + + def key(self): # RFC 9651 §4.2.3.3 + c = self.peek() + if c is None or not (c in LCALPHA or c == "*"): + raise SFFail() + out = "" + while self.peek() is not None and (self.peek() in LCALPHA or self.peek() in DIGIT or self.peek() in "_-.*"): + out += self.take() + return out + + def parameters(self): # §4.2.3.2 + while self.peek() == ";": + self.i += 1 + self.spaces() + self.key() + if self.peek() == "=": + self.i += 1 + self.bare() + + def number(self): # §4.2.4; True for an Integer + kind, digits = "integer", "" + if self.peek() == "-": + self.i += 1 + if self.peek() is None or self.peek() not in DIGIT: + raise SFFail() + while self.peek() is not None: + c = self.take() + if c in DIGIT: + digits += c + elif kind == "integer" and c == ".": + if len(digits) > 12: + raise SFFail() + digits += c + kind = "decimal" + else: + self.i -= 1 + break + if kind == "integer" and len(digits) > 15: + raise SFFail() + if kind == "decimal" and len(digits) > 16: + raise SFFail() + if kind == "decimal": + if digits.endswith(".") or len(digits.split(".")[1]) > 3: + raise SFFail() + return kind == "integer" + + def string(self): # §4.2.5 + self.need('"') + while self.peek() is not None: + c = self.take() + if c == "\\": + n = self.take() + if n not in ('"', "\\"): + raise SFFail() + elif c == '"': + return + elif ord(c) < 0x20 or ord(c) >= 0x7F: + raise SFFail() + raise SFFail() + + def token(self): # §4.2.6 + c = self.peek() + if c is None or not (c in ALPHA or c == "*"): + raise SFFail() + out = "" + while self.peek() is not None and (self.peek() in TCHAR or self.peek() in ":/"): + out += self.take() + return out + + def byte_sequence(self): # §4.2.7, with §3.3's reading of its content + self.need(":") + end = self.s.find(":", self.i) + if end < 0: + raise SFFail() + content, self.i = self.s[self.i:end], end + 1 + body = content.rstrip("=") + pad = len(content) - len(body) + if pad > 2 or any(c not in B64 for c in body) or len(body) % 4 == 1: + raise SFFail() + + def boolean(self): # §4.2.8 + self.need("?") + if self.peek() not in ("0", "1"): + raise SFFail() + self.i += 1 + + def date(self): # §4.2.9 + self.need("@") + if not self.number(): + raise SFFail() + + def display(self): # §4.2.10 + self.need("%") + self.need('"') + out = b"" + while self.peek() is not None: + c = self.take() + if ord(c) < 0x20 or ord(c) >= 0x7F: + raise SFFail() + if c == "%": + h = self.s[self.i:self.i + 2] + if len(h) != 2 or any(x not in "0123456789abcdef" for x in h): + raise SFFail() + out += bytes.fromhex(h) + self.i += 2 + elif c == '"': + try: + out.decode("utf-8") + except UnicodeDecodeError: + raise SFFail() + return + else: + out += c.encode() + raise SFFail() + + def bare(self): # §4.2.3.1; ("token", text) or ("other",) + c = self.peek() + if c is None: + raise SFFail() + if c == "-" or c in DIGIT: + self.number() + return ("other",) + if c == '"': + self.string() + return ("other",) + if c in ALPHA or c == "*": + return ("token", self.token()) + if c == ":": + self.byte_sequence() + return ("other",) + if c == "?": + self.boolean() + return ("other",) + if c == "@": + self.date() + return ("other",) + if c == "%": + self.display() + return ("other",) + raise SFFail() + + def item(self): # §4.2.3 + v = self.bare() + self.parameters() + return v + + def inner_list(self): # §4.2.1.2 + self.need("(") + while self.peek() is not None: + self.spaces() + if self.peek() == ")": + self.i += 1 + self.parameters() + return ("other",) + self.item() + if self.peek() not in (" ", ")"): + raise SFFail() + raise SFFail() + + def dictionary(self): # §4.2.2 + d = {} + while self.peek() is not None: + k = self.key() + if self.peek() == "=": + self.i += 1 + d[k] = self.inner_list() if self.peek() == "(" else self.item() + else: + self.parameters() + d[k] = ("other",) + self.ows() + if self.peek() is None: + return d + if self.take() != ",": + raise SFFail() + self.ows() + if self.peek() is None: + raise SFFail() + return d + + +def parse_dictionary(text): + """§4.2 for a Dictionary: the members, or None when it fails to parse.""" + if any(ord(c) > 0x7F for c in text): + return None + p = SF(text) + try: + p.spaces() + d = p.dictionary() + p.spaces() + except SFFail: + return None + return d if p.peek() is None else None + + +def aipref_prefs(value): + """draft-ietf-aipref-vocab-08 §6.5: each label's preference, or None when it does not parse.""" + d = parse_dictionary(value) + if d is None: + return None + out = {} + for label in LABELS: + m = d.get(label) + out[label] = ("allow" if m == ("token", "y") else "disallow" if m == ("token", "n") else "unknown") + return out + + +# ---------------------------------------------------------------- §3 the statement + +def cawg_key(k): + """§3.4's key form.""" + if not 1 <= len(k.encode()) <= 255: + return False + return all(part and all(c in ALPHA or c in DIGIT or c in "_-" for c in part) for part in k.split(".")) + + +def is_policy(s): + b = s.encode() + return (8 <= len(b) <= 2048 and all(0x21 <= c <= 0x7E for c in b) + and (s.startswith("https://") or s.startswith("http://"))) + + +def read_statement(v): + """§3.6: the statement as {vocabulary: {term: preference}} with its digest, or Refuse.""" + bad = Refuse("usage_malformed") + if not isinstance(v, dict) or not v or any(k not in VOCABULARIES for k in v): + raise bad + prefs, revisions = {}, [] + if "aipref" in v: + a = v["aipref"] + if not isinstance(a, dict) or set(a) != {"revision", "value"}: + raise bad + if not isinstance(a["revision"], str) or not isinstance(a["value"], str): + raise bad + val = a["value"] + if "aipref-length" not in SKIP and not 1 <= len(val) <= 1024: + raise bad + p = aipref_prefs(val) if "aipref-parse" not in SKIP else (aipref_prefs(val) or {x: "unknown" for x in LABELS}) + if p is None or any(ord(c) > 0x7F for c in val): + raise bad + prefs["aipref"] = p + revisions.append((a["revision"], AIPREF_REV)) + if "cawg" in v: + c = v["cawg"] + if not isinstance(c, dict) or set(c) != {"revision", "entries"} or not isinstance(c["revision"], str): + raise bad + e = c["entries"] + if not isinstance(e, dict) or not 1 <= len(e) <= 32: + raise bad + p = {} + for k, x in e.items(): + if not cawg_key(k): + raise bad + if "cawg-reserved" not in SKIP and k.startswith("cawg.") and k not in CAWG_KEYS: + raise bad + if not isinstance(x, dict) or "use" not in x or any(m not in ("use", "constraint_info") for m in x): + raise bad + if x["use"] not in ("allowed", "notAllowed", "constrained"): + raise bad + if "constraint_info" in x and not (isinstance(x["constraint_info"], str) + and 1 <= len(x["constraint_info"].encode()) <= 1024): + raise bad + p[k] = "allow" if x["use"] == "allowed" else "disallow" + prefs["cawg"] = p + revisions.append((c["revision"], CAWG_REV)) + if "tdmrep" in v: + t = v["tdmrep"] + if not isinstance(t, dict) or set(t) != {"revision", "tdm-reservation", "tdm-policy"}: + raise bad + if not isinstance(t["revision"], str): + raise bad + r = t["tdm-reservation"] + if "tdm-values" not in SKIP and not (is_uint(r) and r in (0, 1)): + raise bad + pol = t["tdm-policy"] + if not (pol is None or (isinstance(pol, str) and is_policy(pol))): + raise bad + prefs["tdmrep"] = {TDM_TERM: "disallow" if r == 1 else "allow"} + revisions.append((t["revision"], TDMREP_REV)) + if "revisions" not in SKIP and any(got != want for got, want in revisions): + raise Refuse("usage_revision_unknown") + canonical = jcs(v).encode() + if len(canonical) > 4096: + raise bad + return {"prefs": prefs, "digest": sha(canonical), "canonical": canonical, "policy": v.get("tdmrep", {}).get("tdm-policy")} + + +def statement_text(text): + try: + v = strict_json(text) + except ValueError: + raise Refuse("usage_malformed") + return read_statement(v) + + +def statement_canonical(b): + """§4.2, §4.3: the bytes read and are the canonical form of what they hold.""" + try: + text = b.decode("utf-8") + except UnicodeDecodeError: + raise Refuse("usage_malformed") + s = statement_text(text) + if "canonical-bytes" not in SKIP and s["canonical"] != b: + raise Refuse("usage_malformed") + return s + + +def pref(s, vocabulary, term): + """The preference a statement gives a term, or None when it does not carry the vocabulary.""" + if vocabulary not in s["prefs"]: + return None + return s["prefs"][vocabulary].get(term, "unknown") + + +def statement_report(s): + return {"digest": s["digest"].hex(), "preferences": {v: dict(p) for v, p in s["prefs"].items()}} + + +# ---------------------------------------------------------------- §4.1 the envelope + +def node(a, b): + return b3(NODE + a + b) + + +def group_payload(g): + """jwp-receipts §2.3, the legacy payload.""" + b = GROUP_SIGN + g["track_alias"].to_bytes(8, "big") + g["group_id"].to_bytes(8, "big") + b += bytes.fromhex(g["root_hash"]) + g["leaf_count"].to_bytes(8, "big") + g["joules_total"].to_bytes(8, "big") + for k in ("group_start", "group_end"): + e = g[k].encode() + b += len(e).to_bytes(4, "big") + e + sid = g["signer_id"].encode() + return b + bytes.fromhex(g["signer_pubkey"]) + len(sid).to_bytes(4, "big") + sid + + +def receipts_hold(g, o): + """jwp-receipts §3 steps 1-5, the content hash left to §4.1 step 2: the legacy group + verifies, and the object's unlabelled leaf is included at its position.""" + if "sign_version" in g or "leaf_version" in o: + return False # a labelled form: no case of this corpus uses one + if "group-signature" not in SKIP and ED.verify(bytes.fromhex(g["signer_pubkey"]), group_payload(g), bytes.fromhex(g["sig"])): + return False + n, p = g["leaf_count"], o["merkle_proof"] + i = p["leaf_index"] + depth = 0 if n <= 1 else (n - 1).bit_length() + if (o["track_alias"], o["group_id"]) != (g["track_alias"], g["group_id"]) or o["object_id"] != i or not i < n \ + or len(p["siblings"]) != depth or p["direction_bits"] != i: + return False + cur = b3(LEAF + bytes.fromhex(o["content_hash"])) + for k, sib in enumerate(p["siblings"]): + s = bytes.fromhex(sib) + cur = node(s, cur) if (i >> k) & 1 else node(cur, s) + return cur == bytes.fromhex(g["root_hash"]) + + +def manifest_of(env): + """SPEC §2, §7.1: the manifest of a WAI1 or WAI2 envelope, read with no member named twice.""" + if len(env) < 8 or env[:4] not in (b"WAI1", b"WAI2"): + return None + n = int.from_bytes(env[4:8], "little") + if 8 + n > len(env): + return None + try: + return strict_json(env[8:8 + n].decode("utf-8")) + except (ValueError, UnicodeDecodeError): + return None + + +def hold(env, o, g): + """§4.1: the object held, or Refuse.""" + if not receipts_hold(g, o): + raise Refuse("receipt_invalid") + h = b3(OBJECT_DOMAIN + env) + if "content-hash" not in SKIP and h != bytes.fromhex(o["content_hash"]): + raise Refuse("usage_unbound") + m = manifest_of(env) + if not isinstance(m, dict): + raise Refuse("usage_malformed") + s = read_statement(m["ai_usage"]) if "ai_usage" in m else None + live = m.get("live") + lt = live.get("t") if isinstance(live, dict) else None + t = lt if isinstance(lt, int) and not isinstance(lt, bool) and 0 <= lt < 2 ** 64 else None + return {"hash": h, "statement": s, "t": t} + + +# ---------------------------------------------------------------- §4.2 the room, §4.3 terms + +def read_record(b): + """§4.2's reading.""" + if len(b) < 4: + raise Refuse("truncated") + if b[:4] != ROOM_MAGIC: + raise Refuse("malformed") + if len(b) < 5: + raise Refuse("truncated") + if b[4] != 1: + raise Refuse("unknown_version") + if len(b) < 5 + 32 + 32 + 8 + 2: + raise Refuse("truncated") + room, prev = b[5:37], b[37:69] + effective = int.from_bytes(b[69:77], "little") + n = int.from_bytes(b[77:79], "little") + if len(b) < 79 + n + 64: + raise Refuse("truncated") + if len(b) > 79 + n + 64: + raise Refuse("malformed") + st = statement_canonical(b[79:79 + n]) + return {"room": room, "prev": prev, "effective": effective, "statement": st, "body": b[:79 + n], + "sig": b[79 + n:], "digest": sha(b[:79 + n])} + + +def check_records(d, records): + """§4.2 over a room's records: the list, or (index, reason).""" + out = [] + for i, b in enumerate(records): + try: + r = read_record(b) + except Refuse as e: + return None, (i, e.reason) + if "record-room" not in SKIP and r["room"] != d["room"]: + return None, (i, "wrong_room") + bad = ED.verify(d["authority"], ROOM_DOMAIN + r["body"], r["sig"]) + if bad and "record-signature" not in SKIP: + return None, (i, bad) + if out: + linked = r["prev"] == out[-1]["digest"] and r["effective"] > out[-1]["effective"] + else: + linked = r["prev"] == bytes(32) and r["effective"] == 0 + if not linked and "record-chain" not in SKIP: + return None, (i, "room_usage_unlinked") + out.append(r) + return out, None + + +def terms_of(g): + """§4.3: None for no terms, the statement, or the string "grant_usage_malformed".""" + attrs = dict(LRV.read_attrs(g["attrs"]) or []) + if "ai_usage" not in attrs: + return None + try: + return statement_canonical(attrs["ai_usage"].encode()) + except Refuse: + return "grant_usage_malformed" + + +def check_room(case): + d = LRV.read_descriptor(bytes.fromhex(case["descriptor"])) + grants, gref = LRV.check_grants(d, [bytes.fromhex(x) for x in case["grants"]]) + records, rref = check_records(d, [bytes.fromhex(x) for x in case["records"]]) + return {"d": d, "grants": grants or [], "grants_refused": gref, "records": records, "records_refused": rref} + + +def records_at(room, t): + """§4.2: the record in effect at t, every record where t is None; none when refused.""" + rs = room["records"] or [] + if t is None: + return rs + if "record-in-effect" in SKIP: + return rs[:1] + at = [r for r in rs if r["effective"] <= t] + return at[-1:] + + +def room_outcome(case): + r = check_room(case) + terms = [] + for g in r["grants"]: + t = terms_of(g) + terms.append({"grant": g["digest"].hex(), "principal": g["principal"].hex(), + "terms": None if t is None else t if isinstance(t, str) else t["digest"].hex()}) + ref = lambda e: {"index": e[0], "reason": e[1]} # noqa: E731 + out = {"grants": ref(r["grants_refused"]) if r["grants_refused"] else "accept", + "records": ref(r["records_refused"]) if r["records_refused"] else "accept", "terms": terms} + if r["records"] is not None: + S = 1000000 + out["in_effect"] = [{"t": t, "statements": [x["statement"]["digest"].hex() for x in records_at(r, t)]} + for t in (0, 59 * S, 60 * S, 3600 * S, None)] + return out + + +# ---------------------------------------------------------------- §4.4 fields, §4.5 catalog + +def check_fields(signed, fields): + """§4.4: the unsigned vocabularies, or Refuse.""" + lines = lambda n: [v for k, v in fields if k.lower() == n.lower()] # noqa: E731 + trim = lambda s: s.strip(" \t") # noqa: E731 + unsigned = [] + usage = lines("Content-Usage") + if usage: + if signed is None or "aipref" not in signed["prefs"]: + unsigned.append("aipref") + elif "field-meaning" not in SKIP and aipref_prefs(", ".join(usage)) != signed["prefs"]["aipref"]: + raise Refuse("header_conflicts") + res, pol = lines("tdm-reservation"), lines("tdm-policy") + if res or pol: + if signed is None or "tdmrep" not in signed["prefs"]: + unsigned.append("tdmrep") + else: + want = "1" if signed["prefs"]["tdmrep"][TDM_TERM] == "disallow" else "0" + if len(res) > 1 or len(pol) > 1: + raise Refuse("header_conflicts") + if res and "field-tdm" not in SKIP and trim(res[0]) != want: + raise Refuse("header_conflicts") + if pol and trim(pol[0]) != signed["policy"]: + raise Refuse("header_conflicts") + return unsigned + + +def check_catalog(text, objects): + """§4.5.""" + try: + e = strict_json(text) + except ValueError: + raise Refuse("catalog_conflicts") + if not isinstance(e, dict) or "wai_ai_usage" not in e: + return + try: + s = read_statement(e["wai_ai_usage"]) + except Refuse: + raise Refuse("catalog_conflicts") + if "catalog-compare" not in SKIP and any(o["statement"] and o["statement"]["digest"] != s["digest"] for o in objects): + raise Refuse("catalog_conflicts") + + +# ---------------------------------------------------------------- §5 precedence + +def governing(o, room): + """§5.1.""" + out = [o["statement"]] if o["statement"] else [] + if room is not None and o["room"]: + out += [r["statement"] for r in records_at(room, o["t"])] + return out + + +def preference(vocabulary, term, statements, terms): + """§5.2 for one use; None outside the vocabulary.""" + if term is None: + return None + if terms is not None and "terms" not in SKIP: + p = pref(terms, vocabulary, term) + if p in ("allow", "disallow"): + return p + ps = [pref(s, vocabulary, term) for s in statements] + if "disallow" in ps and "most-restrictive" not in SKIP: + return "disallow" + if "allow" in ps: + return "allow" + return "unknown" + + +def combine(prefs): + """§5.2's last paragraph; None for a disallow.""" + if "disallow" in prefs: + return None + if "allow" in prefs: + return "allowed" + if all(p is None for p in prefs): + return "outside" + return "unknown" + + +# ---------------------------------------------------------------- §6 the claim + +ENVELOPE = ("kind", "class", "subject_type", "subject", "seq", "issued_at", "body", "signer_pubkey", "signer_id", + "parent_receipt_hash", "sig") +BODY = ("objects", "usage", "seen", "grant") +B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + + +def did_key(pk): + """jwp-receipts §7.1: did:key:z and base58btc of 0xed 0x01 and the key.""" + b = b"\xed\x01" + pk + n = int.from_bytes(b, "big") + out = "" + while n: + n, r = divmod(n, 58) + out = B58[r] + out + return "did:key:z" + "1" * (len(b) - len(b.lstrip(b"\x00"))) + out + + +def is_hex(v, n): + return isinstance(v, str) and len(v) == 2 * n and all(c in "0123456789abcdef" for c in v) + + +def is_date_time(s): + """jwp-receipts §2.4.3's date-time.""" + import re + m = re.fullmatch(r"([0-9]{4})-([0-9]{2})-([0-9]{2})[Tt]([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]+)?([Zz]|[+-]([0-9]{2}):([0-9]{2}))", s) + if not m: + return False + y, mo, d, h, mi, se = (int(m.group(i)) for i in range(1, 7)) + leap = y % 4 == 0 and (y % 100 != 0 or y % 400 == 0) + days = [31, 29 if leap else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + if not (1 <= mo <= 12 and 1 <= d <= days[mo - 1] and h <= 23 and mi <= 59 and se <= 60): + return False + return m.group(9) is None or (int(m.group(9)) <= 23 and int(m.group(10)) <= 59) + + +def read_claim(wire): + """jwp-receipts §3.1 steps 1-4 for a consumption claim: the claim, or Refuse(reason, step).""" + def at(step, reason): + return Refuse(reason, step) + try: + c = strict_json(wire) + except ValueError: + raise at(1, "malformed") + if not isinstance(c, dict): + raise at(1, "malformed") + if "kind" not in c: + raise at(1, "missing_required_field") + if c["kind"] != "claim": + raise at(1, "wrong_kind") + if any(k not in c for k in ENVELOPE): + raise at(1, "missing_required_field") + if any(k not in ENVELOPE for k in c): + raise at(1, "unknown_member") + if not isinstance(c["class"], str): + raise at(1, "field_type") + if c["class"] == "delivery": + raise at(1, "class_reserved") + if c["class"] != "consumption": + raise at(1, "unknown_class") + ok = (c["subject_type"] in ("object", "receipt") and is_hex(c["subject"], 32) and is_uint(c["seq"]) + and isinstance(c["issued_at"], str) and is_date_time(c["issued_at"]) and is_hex(c["signer_pubkey"], 32) + and isinstance(c["signer_id"], str) and (c["parent_receipt_hash"] is None or is_hex(c["parent_receipt_hash"], 32)) + and is_hex(c["sig"], 64)) + if not ok: + raise at(1, "field_type") + if c["subject_type"] != "object": + raise at(1, "subject_type_not_admitted") + # Step 2: §6.1. + b = c["body"] + if not isinstance(b, dict): + raise at(2, "field_type") + if any(k not in b for k in BODY): + raise at(2, "missing_required_field") + if any(k not in BODY for k in b): + raise at(2, "unknown_member") + if not (isinstance(b["objects"], list) and 1 <= len(b["objects"]) <= 1024 and all(is_hex(h, 32) for h in b["objects"])): + raise at(2, "field_type") + if not (isinstance(b["usage"], list) and 1 <= len(b["usage"]) <= 3): + raise at(2, "field_type") + for e in b["usage"]: + if not isinstance(e, dict): + raise at(2, "field_type") + if "vocabulary" not in e or "term" not in e: + raise at(2, "missing_required_field") + if len(e) != 2: + raise at(2, "unknown_member") + if e["vocabulary"] not in VOCABULARIES: + raise at(2, "field_type") + if not (e["term"] is None or (isinstance(e["term"], str) and 1 <= len(e["term"].encode()) <= 255)): + raise at(2, "field_type") + if not (isinstance(b["seen"], list) and len(b["seen"]) <= 64 and all(is_hex(h, 32) for h in b["seen"])): + raise at(2, "field_type") + if not (b["grant"] is None or is_hex(b["grant"], 32)): + raise at(2, "field_type") + # Step 3. + unsigned = {k: v for k, v in c.items() if k != "sig"} + why = ED.verify(bytes.fromhex(c["signer_pubkey"]), CLAIM_SIGN + jcs(unsigned).encode(), bytes.fromhex(c["sig"])) + if why == "bad_key": + raise at(3, "bad_signer_key") + if why and "claim-signature" not in SKIP: + raise at(3, "signature_verify_failed") + # Step 4: §6.2, in its order. + asc = lambda xs: all(x < y for x, y in zip(xs, xs[1:])) # noqa: E731 + if "claim-order" not in SKIP and not asc(b["objects"]): + raise at(4, "consumption_objects_unordered") + if c["subject"] != b["objects"][0]: + raise at(4, "consumption_subject_mismatch") + if not asc([e["vocabulary"] for e in b["usage"]]): + raise at(4, "consumption_usage_unordered") + for e in b["usage"]: + t, v = e["term"], e["vocabulary"] + if t is None: + continue + known = (t in LABELS if v == "aipref" else t == TDM_TERM if v == "tdmrep" + else t in CAWG_KEYS or (cawg_key(t) and not t.startswith("cawg."))) + if not known and "claim-terms" not in SKIP: + raise at(4, "consumption_term_unknown") + if not asc(b["seen"]): + raise at(4, "consumption_seen_unordered") + if "claim-did-key" not in SKIP and c["signer_id"] != did_key(bytes.fromhex(c["signer_pubkey"])): + raise at(4, "consumption_signer_not_principal") + return c + + +def link_hash(c): + return b3(CLAIM_LINK + jcs(c).encode()) + + +def verify_claim(wire, objects, room, previous_wire): + """§6.3 after jwp-receipts §3.1 steps 1-5: ("accept", outcomes) or Refuse.""" + c = read_claim(wire) + # Step 5: the previous claim is held only if it passes steps 1-4. + prev = None + if previous_wire is not None: + try: + prev = read_claim(previous_wire) + except Refuse: + prev = None + parent = c["parent_receipt_hash"] + if prev is None and parent is not None and "chain" not in SKIP: + raise Refuse("parent_not_held", 5) + if prev is not None: + if parent is None or prev["signer_pubkey"] != c["signer_pubkey"] or bytes.fromhex(parent) != link_hash(prev) \ + or c["seq"] <= prev["seq"]: + raise Refuse("chain_break", 5) + b = c["body"] + held = [] + for h in b["objects"]: + if h not in objects: + raise Refuse("consumption_object_unknown", 6) + held.append(objects[h]) + terms = None + if b["grant"] is not None and "grant" not in SKIP: + g = None if room is None else next((x for x in room["grants"] if x["digest"].hex() == b["grant"]), None) + good = g is not None and g["principal"].hex() == c["signer_pubkey"] and all( + o["room"] and (o["t"] is None or LRV.good_at(g, room["d"], o["t"])) for o in held) + if not good: + raise Refuse("grant_mismatch", 6) + t = terms_of(g) + terms = t if isinstance(t, dict) else None + rooms = lambda o: room if o["room"] else None # noqa: E731 + seen = sorted({s["digest"].hex() for o in held for s in governing(o, rooms(o))}) + if "seen" not in SKIP and seen != b["seen"]: + raise Refuse("preference_mismatch", 6) + carried = {v for o in held for s in governing(o, rooms(o)) for v in s["prefs"]} + if "answered" not in SKIP and not carried <= {e["vocabulary"] for e in b["usage"]}: + raise Refuse("usage_unanswered", 6) + outcomes = [] + for h, o in zip(b["objects"], held): + g = governing(o, rooms(o)) + r = combine([preference(e["vocabulary"], e["term"], g, terms) for e in b["usage"]]) + if r is None: + raise Refuse("usage_disallowed", 6) + outcomes.append({"object": h, "outcome": r}) + return c, outcomes + + +# ---------------------------------------------------------------- §7 the gate + +def gate(case, keys, results): + """§7.2: None to serve, or the reason.""" + agent = keys[case["agent"]] + allowed, disallowing = [], [] + for name in case["claims"]: + c, r = results[name] + if c is None or c["signer_pubkey"] != agent: + continue + if isinstance(r, list): + allowed.append((c, r)) + elif r == "usage_disallowed": + disallowing.append(c) + w = case["policy"]["window"] + if w == 0: + checked = list(case["request"]) + else: + served = case["served"] + checked = [h for grp in served[:max(len(served) - w, 0)] for h in grp] + for h in checked: + names = lambda c: h in c["body"]["objects"] # noqa: E731 + if not any(names(c) for c, _ in allowed) and not any(names(c) for c in disallowing): + return "claim_required" + if any(names(c) for c in disallowing): + return "usage_disallowed" + if case["policy"]["require_allowed"] and "require-allowed" not in SKIP and any( + o["object"] == h and o["outcome"] != "allowed" for _, r in allowed for o in r): + return "usage_not_granted" + return None + + +# ---------------------------------------------------------------- the corpus + +def held_objects(doc): + """The held objects of each publication, by content hash.""" + groups = {p["name"]: p for p in doc["publications"]} + out = {} + for case in doc["objects"]: + p = groups[case["publication"]] + try: + o = hold(bytes.fromhex(case["envelope"]), case["receipt"], p["group"]) + except Refuse: + continue + o["room"] = p["room"] is not None + out.setdefault(case["publication"], {})[o["hash"].hex()] = o + return out + + +def object_named(doc, held, publication, name): + case = next(o for o in doc["objects"] if o["publication"] == publication and o["name"] == name) + return held[publication][b3(OBJECT_DOMAIN + bytes.fromhex(case["envelope"])).hex()] + + +def outcomes(doc): + """Every case's outcome, by section, from the document's inputs.""" + out = {} + res = [] + for case in doc["statements"]: + try: + r = statement_report(statement_text(case["json"])) + r["outcome"] = "accept" + except Refuse as e: + r = {"outcome": "refuse", "reason": e.reason} + res.append(r) + out["statements"] = res + groups = {p["name"]: p for p in doc["publications"]} + res = [] + for case in doc["objects"]: + try: + o = hold(bytes.fromhex(case["envelope"]), case["receipt"], groups[case["publication"]]["group"]) + res.append({"outcome": "held", "content_hash": o["hash"].hex(), + "statement": o["statement"]["digest"].hex() if o["statement"] else None, "t": o["t"]}) + except Refuse as e: + res.append({"outcome": "refuse", "reason": e.reason}) + out["objects"] = res + out["rooms"] = [room_outcome(case) for case in doc["rooms"]] + held = held_objects(doc) + res = [] + for case in doc["responses"]: + o = object_named(doc, held, case["publication"], case["object"]) + try: + res.append({"outcome": "accept", "unsigned": check_fields(o["statement"], [tuple(f) for f in case["fields"]])}) + except Refuse as e: + res.append({"outcome": "refuse", "reason": e.reason}) + out["responses"] = res + res = [] + for case in doc["catalogs"]: + os_ = [object_named(doc, held, case["publication"], n) for n in case["objects"]] + try: + check_catalog(case["entry"], os_) + res.append({"outcome": "accept"}) + except Refuse as e: + res.append({"outcome": "refuse", "reason": e.reason}) + out["catalogs"] = res + rooms = {case["name"]: case for case in doc["rooms"]} + wires, results, res = {}, {}, [] + for case in doc["claims"]: + ctx = case["context"] + objects = {} + for p in ctx["publications"]: + objects.update(held.get(p, {})) + room = check_room(rooms[ctx["room"]]) if ctx["room"] else None + prev = wires[ctx["previous"]] if ctx["previous"] else None + try: + c, outs = verify_claim(case["wire"], objects, room, prev) + res.append({"outcome": "accept", "objects": outs}) + results[case["name"]] = (c, outs) + except Refuse as e: + res.append({"outcome": "refuse", "step": e.step, "reason": e.reason}) + claim = None + if e.step == 6: + claim = read_claim(case["wire"]) + results[case["name"]] = (claim, e.reason) + wires[case["name"]] = case["wire"] + out["claims"] = res + keys = {k: v for k, v in doc["keys"].items()} + res = [] + for case in doc["gates"]: + r = gate(case, keys, results) + res.append({"outcome": "serve"} if r is None else {"outcome": "refuse", "reason": r}) + out["gates"] = res + return out + + +SECTIONS = ("statements", "objects", "rooms", "responses", "catalogs", "claims", "gates") + + +def check(doc, quiet=False): + """The failures: every case whose stated outcome is not the one this verifier reaches.""" + fails = [] + try: + got = outcomes(doc) + except (Refuse, KeyError, ValueError, TypeError, StopIteration, AttributeError, LEV.Refuse) as e: + return ["the corpus does not read: %r" % e] + for s in SECTIONS: + if len(got[s]) != len(doc[s]): + fails.append("%s: %d cases, %d outcomes" % (s, len(doc[s]), len(got[s]))) + for case, out in zip(doc[s], got[s]): + if case.get("outcome") != out: + fails.append("%s/%s: stated %s, got %s" % (s, case["name"], json.dumps(case.get("outcome"))[:120], + json.dumps(out)[:120])) + if not quiet: + for s in SECTIONS: + bad = [f for f in fails if f.startswith(s + "/")] + print(("PASS" if not bad else "FAIL") + " %s: %d cases" % (s, len(doc[s]))) + return fails + + +def reasons_given(doc): + seen = set() + for s in SECTIONS: + for case in doc[s]: + o = case["outcome"] + if s == "rooms": + for k in ("records", "grants"): + if isinstance(o[k], dict): + seen.add(o[k]["reason"]) + seen.update(t["terms"] for t in o["terms"] if isinstance(t["terms"], str) and len(t["terms"]) != 64) + elif "reason" in o: + seen.add(o["reason"]) + return seen + + +def flip_hex(h, at): + """A hex string with one digit changed.""" + c = "0" if h[at] != "0" else "1" + return h[:at] + c + h[at + 1:] + + +def mutations(doc): + """Copies of the corpus, each with one defect a conforming verifier catches.""" + def named(d, s, name): + return next(c for c in d[s] if c["name"] == name) + + def m(label, f): + d = copy.deepcopy(doc) + f(d) + return label, d + + def set_(s, name, path, value): + def f(d): + x = named(d, s, name) + for k in path[:-1]: + x = x[k] + x[path[-1]] = value + return f + + def edit(s, name, key, old, new): + def f(d): + x = named(d, s, name) + assert old in x[key], (s, name, old) + x[key] = x[key].replace(old, new, 1) + return f + + yield m("a statement's stated preference", set_("statements", "aipref_only", ["outcome", "preferences", "aipref", "train-ai"], "allow")) + yield m("a statement's value", edit("statements", "aipref_only", "json", "train-ai=n", "train-ai=y")) + yield m("an unknown revision stated accepted", set_("statements", "aipref_revision_unknown", ["outcome"], {"outcome": "accept"})) + yield m("an object's payload byte", lambda d: named(d, "objects", "feature").update( + envelope=flip_hex(named(d, "objects", "feature")["envelope"], len(named(d, "objects", "feature")["envelope"]) - 2))) + yield m("an object's stated statement", set_("objects", "trailer", ["outcome", "statement"], None)) + yield m("a group receipt's signature", lambda d: d["publications"][0]["group"].update( + sig=flip_hex(d["publications"][0]["group"]["sig"], 10))) + yield m("a room record's signature", lambda d: named(d, "rooms", "lecture")["records"].__setitem__( + 1, flip_hex(named(d, "rooms", "lecture")["records"][1], len(named(d, "rooms", "lecture")["records"][1]) - 4))) + yield m("a room's stated records in effect", lambda d: named(d, "rooms", "lecture").__setitem__( + "outcome", dict(named(d, "rooms", "lecture")["outcome"], in_effect=[]))) + yield m("a response's field", lambda d: named(d, "responses", "respelled")["fields"].__setitem__(0, ["Content-Usage", "train-ai=y"])) + yield m("a catalog's statement", edit("catalogs", "agrees", "entry", '"tdm-reservation": 1', '"tdm-reservation": 0')) + def flip_sig(d): + x = named(d, "claims", "inference_allowed") + at = x["wire"].index('"sig":"') + len('"sig":"') + 5 + x["wire"] = flip_hex(x["wire"], at) + yield m("a claim's signature", flip_sig) + yield m("a claim's stated object outcome", set_("claims", "unknown_use", ["outcome", "objects", 0, "outcome"], "allowed")) + yield m("a claim's context room", set_("claims", "room_terms_lift", ["context", "room"], None)) + yield m("a claim's previous", set_("claims", "unknown_use", ["context", "previous"], None)) + yield m("a gate's window", set_("gates", "claim_behind_past_window", ["policy", "window"], 2)) + yield m("a gate's policy", set_("gates", "unknown_not_granted", ["policy", "require_allowed"], False)) + yield m("a gate's claims", lambda d: named(d, "gates", "claim_ahead_disallowed").update(claims=["inference_allowed"])) + + +CHECKS = { + "aipref-length": "§3.2: the length of an aipref value", + "aipref-parse": "§3.3: a value that does not parse is refused", + "cawg-reserved": "§3.4: the reserved cawg. prefix", + "tdm-values": "§3.5: tdm-reservation is 0 or 1", + "revisions": "§3.6 step 2: the revisions", + "canonical-bytes": "§4.2, §4.3: the bytes are the canonical form", + "group-signature": "§4.1 step 1: the group receipt's signature", + "content-hash": "§4.1 step 2: the envelope hashes to its receipt", + "record-room": "§4.2: a record names the room", + "record-signature": "§4.2: the authority signs a record", + "record-chain": "§4.2: records are chained", + "record-in-effect": "§4.2: the record in effect at an instant", + "field-meaning": "§4.4: Content-Usage compared by meaning", + "field-tdm": "§4.4: tdm-reservation compared", + "catalog-compare": "§4.5: the catalog compared with the objects", + "terms": "§5.2 rule 1: a grant's terms", + "most-restrictive": "§5.2 rule 2: the most restrictive", + "claim-signature": "jwp-receipts §3.1 step 3: the claim's signature", + "claim-order": "§6.2 rule 1: objects in order", + "claim-terms": "§6.2 rule 4: the vocabularies' own terms", + "claim-did-key": "§6.2 rule 6: the signer's did:key", + "chain": "jwp-receipts §3.1 step 5: the parent is held", + "grant": "§6.3 step 2: the grant", + "seen": "§6.3 step 3: the statements seen", + "answered": "§6.3 step 4: every vocabulary answered", + "require-allowed": "§7.2 step 3: require allowed", +} + + +def main(corpus_dir): + fails = [] + st = ED._self_test() + print(("PASS" if not st else "FAIL") + " Ed25519 strict rule self-test " + ",".join(st)) + fails += ["ed25519 self-test"] if st else [] + print(("PASS" if len(REASONS) == 21 else "FAIL") + " §9 read from the extension: %d reasons" % len(REASONS)) + fails += [] if len(REASONS) == 21 else ["§9 does not read"] + with open(os.path.join(corpus_dir, "vectors.json"), encoding="utf-8") as f: + doc = json.load(f) + fails += check(doc) + extra = sorted(set(os.listdir(corpus_dir)) - {"vectors.json", "README.md"}) + print(("PASS" if not extra else "FAIL") + " no file but vectors.json and README.md") + fails += ["an extra file: %s" % n for n in extra] + missing = [r for r in REASONS if r not in reasons_given(doc)] + print(("PASS" if not missing else "FAIL") + " every reason of §9 appears in the corpus") + fails += ["no case gives %s" % r for r in missing] + for label, d in mutations(doc): + caught = check(d, quiet=True) + print(("PASS" if caught else "FAIL") + " mutated copy fails: %s%s" % (label, " (%s)" % caught[0][:90] if caught else "")) + if not caught: + fails.append("a mutated copy verifies: %s" % label) + for name, what in CHECKS.items(): + SKIP.clear() + SKIP.add(name) + caught = check(doc, quiet=True) + SKIP.clear() + print(("PASS" if caught else "FAIL") + " a verifier without %s fails the corpus%s" % (what, " (%s)" % caught[0][:70] if caught else "")) + if not caught: + fails.append("the corpus does not catch a verifier without %s" % what) + for f in fails: + print("FAIL " + f) + print(("PASS" if not fails else "FAIL") + " AI usage corpus: %s" % ", ".join("%d %s" % (len(doc[s]), s) for s in SECTIONS)) + return 1 if fails else 0 + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("usage: ai_usage_verify.py ", file=sys.stderr) + sys.exit(2) + sys.exit(main(sys.argv[1])) diff --git a/wai/live-conformance/usage/README.md b/wai/live-conformance/usage/README.md new file mode 100644 index 000000000..d5ae85002 --- /dev/null +++ b/wai/live-conformance/usage/README.md @@ -0,0 +1,44 @@ +# live-conformance/usage — AI usage preferences and consumption claims + +The corpus of [`extensions/ai-usage.md`](../../extensions/ai-usage.md) §10, in one file, +`vectors.json`. Its `keys` name every key by its role, as 64 lowercase hexadecimal digits; its +`revisions` are the vocabulary revisions §3 reads. Each section is a list of cases, each with +its `name`, a `description`, its inputs and its `outcome`: + +- `statements` (§3): `json`, a statement's text. `accept`: the `digest` and the `preferences` + of every term it names, by vocabulary (`allow`, `disallow` or `unknown`); or `refuse` and the + `reason`. +- `publications`: each a group of objects under one key, its `group` receipt + ([jwp-receipts](../../extensions/jwp-receipts.md) §2.4.1) and, for a room's media, its + `room`. +- `objects` (§4.1): `envelope` (hexadecimal) and its object `receipt`, under its publication's + group. `held`: the `content_hash`, the `statement`'s digest or `null`, and `t`, the manifest's + `live.t` or `null`; or `refuse` and the `reason`. +- `rooms` (§4.2, §4.3): `descriptor`, `grants` and usage `records`, each hexadecimal. The + outcome holds `grants` and `records`, each `accept` or the `index` and `reason` of the first + refused; `terms`, each grant the room accepts with its `principal` and its terms' digest, + `null` for none or `grant_usage_malformed`; and, where the records are accepted, `in_effect`: + the statements governing an object at each instant `t` (`null` for an object with no time). +- `responses` (§4.4): an object of a publication and its response `fields`, each a name and a + value. `accept` and the vocabularies the fields state `unsigned`, or `refuse` and the + `reason`. +- `catalogs` (§4.5): a catalog track `entry` and the `objects` of its track. +- `claims` (§6): the claim's `wire` text and its `context`: the `publications` whose held + objects the verifier holds, the `room`, and the claim before it of its signer, `previous`, by + name, or `null` when the verifier is given none. `accept` and each object's `outcome` + (`allowed`, `unknown` or `outside`), in the order of the claim's `objects`; or `refuse`, the + jwp-receipts §3.1 `step` and the `reason`. +- `gates` (§7): the `policy`, the `agent` by its key's name, the groups `served`, the `claims` + given, by name, each verified with its own context, and the group requested. `serve`, or + `refuse` and the `reason`. + +A verifier that reaches another outcome, reason, step, digest, preference, terms or object +outcome does not conform. Payloads are opaque test bytes that no case decodes. + +**Provenance.** Generated here: `wai_ai_usage_vectors gen` writes the file from statements, +envelopes, rooms and claims authored in code, from fixed seeds. Each key is SHA-256 of a fixed +label and an index; every time is a constant (the room's `t0` is 2026-10-21T00:00:00Z); every +origin and URL is an `.example` name (RFC 2606); and Ed25519 signing is deterministic (RFC +8032), so the file is the same bytes on every machine. The generator asserts that each case +reaches the outcome it was written to have and that the corpus gives every reason of §9. +`ai_usage_verify.py`, beside this directory, is the second implementation. diff --git a/wai/registry/capabilities.json b/wai/registry/capabilities.json index f3a940393..217a89640 100644 --- a/wai/registry/capabilities.json +++ b/wai/registry/capabilities.json @@ -1,6 +1,20 @@ { "schema": "https://git.transaction.science/Transaction-Science/open-standards/src/branch/main/wai/registry/capabilities.schema.json", "capabilities": [ + { + "capability": "wai.ai.usage", + "kind": "record", + "status": "registered", + "media": null, + "role": null, + "tier": null, + "licence": null, + "replicate": null, + "sets": [], + "spec_section": "§5 Interactive, temporal & computed content", + "edition": null, + "registered": "2026-10-21" + }, { "capability": "wai.asset.provenance", "kind": "record", @@ -1920,6 +1934,13 @@ "section": "§10.5.3", "status": "proposed" }, + { + "name": "WAI-Usage-Claim", + "kind": "http-header", + "document": "extensions/ai-usage.md", + "section": "§7.3", + "status": "proposed" + }, { "name": "WAI1", "kind": "magic", @@ -1955,6 +1976,13 @@ "section": "§3.1", "status": "defined" }, + { + "name": "WAU1", + "kind": "magic", + "document": "extensions/ai-usage.md", + "section": "§4.2", + "status": "defined" + }, { "name": "WCD1", "kind": "magic", @@ -2347,6 +2375,13 @@ "section": "Appendix B", "status": "provisional" }, + { + "name": "wai_ai_usage", + "kind": "catalog", + "document": "extensions/moq-streaming-format.md", + "section": "§4", + "status": "defined" + }, { "name": "wai_capabilities", "kind": "catalog", diff --git a/wai/vocab/v2.ttl b/wai/vocab/v2.ttl index 7dc643ce1..d65a7ed8a 100644 --- a/wai/vocab/v2.ttl +++ b/wai/vocab/v2.ttl @@ -103,6 +103,13 @@ wai:pinTier a owl:DatatypeProperty ; rdfs:domain wai:ParameterSetPin ; rdfs:rang # ─── Registered strings ────────────────────────────────────────────── +wai:wai.ai.usage a wai:Capability ; + rdfs:label "wai.ai.usage" ; + wai:kind wai:kind-record ; + wai:status wai:status-registered ; + wai:specSection "§5 Interactive, temporal & computed content" ; + wai:registeredOn "2026-10-21"^^xsd:date . + wai:wai.asset.provenance a wai:Capability ; rdfs:label "wai.asset.provenance" ; wai:kind wai:kind-record ; diff --git a/wai/wai-rs/src/claim_receipt.rs b/wai/wai-rs/src/claim_receipt.rs index e02945611..c911cacdf 100644 --- a/wai/wai-rs/src/claim_receipt.rs +++ b/wai/wai-rs/src/claim_receipt.rs @@ -127,6 +127,7 @@ pub const DOMAIN_CLAIM_LINK: &[u8] = b"moq-jwp:claim-link\x01"; // names here so every existing caller of this module's API stays valid. pub use crate::jwp_hash::{content_hash as object_content_hash, DOMAIN_OBJECT}; +/// cbindgen:ignore pub mod consumption; pub use consumption::{ConsumptionClaim, UsageEntry, Vocabulary}; pub mod session; diff --git a/wai/wai-rs/src/claim_receipt/consumption.rs b/wai/wai-rs/src/claim_receipt/consumption.rs index 00f38fca9..8afacca39 100644 --- a/wai/wai-rs/src/claim_receipt/consumption.rs +++ b/wai/wai-rs/src/claim_receipt/consumption.rs @@ -14,10 +14,16 @@ const CONSUMPTION_MEMBERS: &[&str] = &["objects", "usage", "seen", "grant"]; /// A `usage` entry's members. const USAGE_MEMBERS: &[&str] = &["vocabulary", "term"]; /// The most objects one claim names. +/// +/// cbindgen:ignore pub const MAX_OBJECTS: usize = 1024; /// The most statements one claim says it saw. +/// +/// cbindgen:ignore pub const MAX_SEEN: usize = 64; /// The longest term, in bytes. +/// +/// cbindgen:ignore pub const MAX_TERM: usize = 255; /// The three vocabularies a usage statement carries (ai-usage §3.1), in the order a claim @@ -33,6 +39,7 @@ pub enum Vocabulary { } impl Vocabulary { + /// cbindgen:ignore pub const ALL: [Vocabulary; 3] = [Vocabulary::Aipref, Vocabulary::Cawg, Vocabulary::Tdmrep]; pub fn label(self) -> &'static str { diff --git a/wai/wai-rs/src/codecs/registry.rs b/wai/wai-rs/src/codecs/registry.rs index 4cd0e0c41..09529bdc4 100644 --- a/wai/wai-rs/src/codecs/registry.rs +++ b/wai/wai-rs/src/codecs/registry.rs @@ -221,6 +221,7 @@ pub const CAP_LIVE_MODERATION: &str = "wai.live.moderation"; pub const CAP_LIVE_E2EE: &str = "wai.live.e2ee"; pub const CAP_LIVE_WATERMARK: &str = "wai.live.watermark"; pub const CAP_LIVE_ADS: &str = "wai.live.ads"; +pub const CAP_AI_USAGE: &str = "wai.ai.usage"; /// One row per string SPEC §5 registers that is a payload, companion or record: /// `(string, SPEC section, edition, registered)`. @@ -310,6 +311,7 @@ const ROWS: &[(&str, &str, Option<&str>, &str)] = &[ (CAP_LIVE_E2EE, S_INTERACTIVE, None, "2026-10-19"), (CAP_LIVE_WATERMARK, S_INTERACTIVE, None, "2026-10-20"), (CAP_LIVE_ADS, S_INTERACTIVE, None, "2026-10-20"), + (CAP_AI_USAGE, S_INTERACTIVE, None, "2026-10-21"), (CAP_AUDIO_IAMF, S_IMMERSIVE, None, "2026-06-17"), (CAP_AUDIO_AC4, S_IMMERSIVE, None, "2026-06-17"), (CAP_AUDIO_IVAS, S_IMMERSIVE, None, "2026-06-17"), @@ -362,6 +364,7 @@ const RECORDS: &[&str] = &[ CAP_LIVE_METERING, CAP_LIVE_STEERING, CAP_TEXT_TIMED, CAP_LIVE_MODERATION, CAP_LIVE_E2EE, CAP_LIVE_WATERMARK, CAP_LIVE_ADS, + CAP_AI_USAGE, ]; /// A named identifier SPEC uses that has the shape of a capability string but is @@ -517,6 +520,7 @@ const ENERGY_STEERING: &str = "extensions/energy-steering.md"; const LIVE_MODERATION: &str = "extensions/live-moderation.md"; const CONFIDENTIALITY: &str = "extensions/confidentiality.md"; const FORENSIC_WATERMARK: &str = "extensions/forensic-watermark.md"; +const AI_USAGE: &str = "extensions/ai-usage.md"; const LIVE_ADS: &str = "extensions/live-ads.md"; const fn wire(name: &'static str, kind: WireKind, document: &'static str, section: &'static str, @@ -528,8 +532,8 @@ const fn wire(name: &'static str, kind: WireKind, document: &'static str, sectio /// (http-delivery, moq-streaming-format), of the key discovery those bindings serve /// (jwp-receipts §7), of packaging, of live events, of live rooms, of capture binding, of /// live composition, of live recording, of timed text, of live metering, of energy steering, -/// of live moderation, of confidentiality, of forensic watermark sessions and of live ads, in -/// document order. The export lists them sorted by name and kind. +/// of live moderation, of confidentiality, of forensic watermark sessions, of live ads and of AI +/// usage, in document order. The export lists them sorted by name and kind. pub const WIRE_IDENTIFIERS: &[WireIdentifier] = &[ wire("WAI1", WireKind::Magic, "SPEC.md", "§2", WireStatus::Defined), wire("WAI2", WireKind::Magic, "SPEC.md", "§7.1", WireStatus::Defined), @@ -615,6 +619,9 @@ pub const WIRE_IDENTIFIERS: &[WireIdentifier] = &[ wire("WWD1", WireKind::Magic, FORENSIC_WATERMARK, "§8.1", WireStatus::Defined), wire("WAP1", WireKind::Magic, LIVE_ADS, "§3.1", WireStatus::Defined), wire("WAB1", WireKind::Magic, LIVE_ADS, "§4.2", WireStatus::Defined), + wire("WAU1", WireKind::Magic, AI_USAGE, "§4.2", WireStatus::Defined), + wire("wai_ai_usage", WireKind::Catalog, MOQ_FORMAT, "§4", WireStatus::Defined), + wire("WAI-Usage-Claim", WireKind::HttpHeader, AI_USAGE, "§7.3", WireStatus::Proposed), ]; /// A string with the shape of a capability that the reference implementation uses -- 2.54.0 (Apple Git-157) From 79f54b9dcdb3d0d9e834960115ae42fb8ae8e40d Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Wed, 21 Oct 2026 05:25:43 -0400 Subject: [PATCH 3/7] wai.ai.usage: changelog, the live-conformance directory description, and plain wording for the gate's window Co-Authored-By: Claude Opus 5.5 --- wai/CHANGELOG.md | 51 +++++++++++++++++++++++++ wai/extensions/ai-usage.md | 4 +- wai/live-conformance/usage/vectors.json | 4 +- wai/wai-rs/src/ai_usage/corpus.rs | 4 +- wai/wai-rs/tests/int_kernels_vectors.rs | 2 +- 5 files changed, 58 insertions(+), 7 deletions(-) diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index e7c07ec03..53466f1af 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -6,6 +6,57 @@ wire format may change and such changes are recorded here. ## v0.4.0 — current +### `wai.ai.usage`: AI usage preferences bound to the bytes, and the agent's consumption claim + +- **The extension** ([ai-usage](extensions/ai-usage.md)) registers the record `wai.ai.usage`. + A **usage statement** carries three published vocabularies verbatim, each in its own member: + `aipref`, a statement of preference of `draft-ietf-aipref-vocab-08` in its Structured Field + form (`train-ai`, `ai-use`, `search`); `tdmrep`, the TDM Reservation Protocol's + `tdm-reservation` and `tdm-policy` (Final Community Group Report of 10 May 2024); and `cawg`, + the entries of a `cawg.training-mining` assertion (Training and Data Mining Assertion 1.1). + WAI adds no category and maps no term of one onto another. Each member names the revision it + is read at, and a revision this one does not read is refused. +- **RFC 9651's Dictionary**, parsed in full: every bare item type, parameters and inner lists, + with a repeated key keeping its last value. A value that does not parse, which the vocabulary + would read as unknown throughout, is refused rather than carried. +- **Bound to the bytes.** The manifest member `ai_usage` puts the statement inside the envelope, + so the object's receipt signs it with the content: an object whose statement is removed or + altered no longer hashes to its receipt (`usage_unbound`). A room states its preference in a + room usage record, `WAU1`, signed by the room authority and chained by the instant it takes + effect, so a change never reaches what was published before it; the descriptor's layout is + unchanged. A grant's attribute `ai_usage` carries the terms one principal is granted. +- **HTTP and MoQ.** An origin writes `draft-ietf-aipref-attach-05`'s `Content-Usage` field and + the TDMRep fields from the signed statement (http-delivery §5.6); a client compares them by + meaning, so a respelling is not a conflict and a contradiction is (`header_conflicts`). A MoQ + catalog states a track's statement as `wai_ai_usage` (moq-streaming-format §4), checked the + same way (`catalog_conflicts`). +- **One precedence rule.** A grant's terms decide the uses they name; otherwise every signed + statement that governs the object, the most restrictive governing, as the vocabulary combines + statements; unsigned statements count only where nothing is signed. CAWG's `constrained` + reads as not allowed unless a grant says otherwise. +- **The consumption claim**: the jwp-receipts claim class `consumption` (§2.4.13 there), signed + by the agent under its `did:key`. It names the objects by content hash, the term it acted + under in every vocabulary their statements carry (or `null`, a use outside it), the statements + it saw and the grant it held. A verifier checks it against the objects, their statements and + the room, and refuses a use they disallow (`usage_disallowed`); the refusal is of the use, and + the claim stays its signer's statement. +- **The gate.** A relay or gateway MAY require claims before it serves an agent: claimed before + the fetch over HTTP, where an object's content hash is known first (the proposed request field + `WAI-Usage-Claim`), or within a window of groups over MOQT; and MAY require every use to be + allowed. +- **Reference and corpus.** `wai-rs` feature `ai_usage` (`src/ai_usage.rs`, with RFC 9651's + parser in `src/ai_usage/sf.rs`), and the class's body and rules in + `src/claim_receipt/consumption.rs`. `live-conformance/usage/`, written from fixed seeds by + `wai_ai_usage_vectors`: statements, receipted envelopes (a `WAI2` among them) with a stripped + and an altered statement, a room with chained records and agent grants, responses, catalogs, + claims accepted and refused at each rule, and gates. Checked by `tests/ai_usage.rs` and by + `live-conformance/ai_usage_verify.py`, a second implementation that shares no code with + `wai-rs`, parses Structured Fields, canonical forms, receipts and claims itself, requires every + mutated copy of the corpus to fail and every weakened copy of itself to be caught; + `claim-conformance/claim_verify.py` reads the class's steps 1 to 4. The registry lists the + record, the magic `WAU1`, the catalog field `wai_ai_usage` and the proposed field + `WAI-Usage-Claim`. + ### wai-quantum 0.6.12: cheaper contraction trees, and the cut-rank table on the GPU 0.6.11 was not published on its own: this release carries it to the package registry, and `wai-quantum-cli` 0.1.0 requires it, since the command line's `metal` feature turns on `quantum_cutrank_metal`. diff --git a/wai/extensions/ai-usage.md b/wai/extensions/ai-usage.md index db82e2882..d1c118f08 100644 --- a/wai/extensions/ai-usage.md +++ b/wai/extensions/ai-usage.md @@ -402,8 +402,8 @@ A relay or a gateway MAY require consumption claims before it serves an agent. W and how it recognises an agent outside a room, is its own policy; in a room, an agent principal is one whose grant carries terms (§4.3). The policy has two parameters: -- `window`, 0 to 16: how many of the most recent groups the gate served the agent may be - unclaimed. With 0, every object the agent requests is claimed before it is served, as over +- `window`, 0 to 16: how many of the most recent groups the gate served the agent may lack a + claim. With 0, every object the agent requests is claimed before it is served, as over HTTP, where the agent names each object by its content hash before it fetches it ([http-delivery](http-delivery.md) §5.2). With *n* ≥ 1, the agent claims what it received after it received it, as over MOQT, where a live object's content hash is known only once it diff --git a/wai/live-conformance/usage/vectors.json b/wai/live-conformance/usage/vectors.json index 2743aa9d0..0fd9e00b2 100644 --- a/wai/live-conformance/usage/vectors.json +++ b/wai/live-conformance/usage/vectors.json @@ -1982,7 +1982,7 @@ } }, { - "name": "claim_ahead_unclaimed", + "name": "claim_ahead_no_claim", "description": "a request for an object no claim names", "policy": { "window": 0, @@ -2064,7 +2064,7 @@ }, { "name": "claim_behind_within_window", - "description": "window 1: the last group served may be unclaimed", + "description": "window 1: the last group served may lack a claim", "policy": { "window": 1, "require_allowed": false diff --git a/wai/wai-rs/src/ai_usage/corpus.rs b/wai/wai-rs/src/ai_usage/corpus.rs index 0262f5a24..3a8cb60ff 100644 --- a/wai/wai-rs/src/ai_usage/corpus.rs +++ b/wai/wai-rs/src/ai_usage/corpus.rs @@ -828,12 +828,12 @@ fn gate_inputs(c: &Corpus) -> Vec { }; vec![ case("claim_ahead_served", "the crawler claimed the feature before requesting it", 0, false, "crawler", vec![], &["inference_allowed"], vec![cat("feature")], "serve"), - case("claim_ahead_unclaimed", "a request for an object no claim names", 0, false, "crawler", vec![], &["inference_allowed"], vec![cat("credits")], "claim_required"), + case("claim_ahead_no_claim", "a request for an object no claim names", 0, false, "crawler", vec![], &["inference_allowed"], vec![cat("credits")], "claim_required"), case("claim_ahead_disallowed", "a request for an object the crawler also claimed for a use the statement disallows", 0, false, "crawler", vec![], &["inference_allowed", "training_forbidden"], vec![cat("feature")], "usage_disallowed"), case("unknown_served", "an unknown preference is served when the policy does not require allowed", 0, false, "crawler", vec![], &["unknown_use"], vec![cat("trailer")], "serve"), case("unknown_not_granted", "an unknown preference under a policy that requires allowed", 0, true, "crawler", vec![], &["inference_allowed", "unknown_use"], vec![cat("trailer")], "usage_not_granted"), - case("claim_behind_within_window", "window 1: the last group served may be unclaimed", 1, false, "crawler", vec![vec![cat("feature")], vec![cat("trailer")]], + case("claim_behind_within_window", "window 1: the last group served may lack a claim", 1, false, "crawler", vec![vec![cat("feature")], vec![cat("trailer")]], &["inference_allowed"], vec![cat("credits")], "serve"), case("claim_behind_past_window", "window 1: an earlier group served and never claimed", 1, false, "crawler", vec![vec![cat("feature")], vec![cat("trailer")], vec![cat("credits")]], &["inference_allowed"], vec![cat("stills")], "claim_required"), diff --git a/wai/wai-rs/tests/int_kernels_vectors.rs b/wai/wai-rs/tests/int_kernels_vectors.rs index 65f3306dd..377d623ce 100644 --- a/wai/wai-rs/tests/int_kernels_vectors.rs +++ b/wai/wai-rs/tests/int_kernels_vectors.rs @@ -223,7 +223,7 @@ fn every_decoded_output_states_a_sha256() { ("pin-conformance", "parameter-set pins: every digest there is a SHA-256 of a parameter set"), ("ops-conformance", "integer operators: every operator, stage and graph vector, the lower_* decodes of the sets under sets/ among them, states its output integers in full or its refusal code, never a digest of them; certificates/ states each lowered set's SHA-256 (a parameter-set identity) and holds the lowered graphs as bytes, compared byte for byte; the output digests a capability decoded through its lowering reproduces (integer-operators §14.4) are stated by that capability's own corpus"), ("signer-conformance", "key logs, grants and statuses: signed documents, no decoded output"), - ("live-conformance", "live events, rooms, takes, compositions, recordings, captions, metering, steering, moderation, watermark sessions and ads: signed records, commitments, packages, clips, cues and their renderings, per-hop records, ledgers and session receipts with their units and joules recomputed, steering policies and decisions with each pathway's evidence and the steering manifests recomputed byte for byte, review policies, decisions, notices and the items' timelines with each statement-of-reasons export written byte for byte, watermark tokens, session records, recovered sequences and disclosures, ad policies, breaks with their interstitial tags, events and asset lists written byte for byte and their impression receipts, and their verdicts, and layouts and mixes stated in full, rectangle for rectangle and sample for sample; nothing decoded against a parameter set"), + ("live-conformance", "live events, rooms, takes, compositions, recordings, captions, metering, steering, moderation, watermark sessions, ads and AI usage: signed records, commitments, packages, clips, cues and their renderings, per-hop records, ledgers and session receipts with their units and joules recomputed, steering policies and decisions with each pathway's evidence and the steering manifests recomputed byte for byte, review policies, decisions, notices and the items' timelines with each statement-of-reasons export written byte for byte, watermark tokens, session records, recovered sequences and disclosures, ad policies, breaks with their interstitial tags, events and asset lists written byte for byte and their impression receipts, usage statements with every term's preference, receipted envelopes (whose payloads are opaque test bytes no case decodes), room usage records, HTTP fields, catalogs, consumption claims and gate decisions, and their verdicts, and layouts and mixes stated in full, rectangle for rectangle and sample for sample; nothing decoded against a parameter set"), ("http-conformance", "objects and head proofs for transport: no case decodes a payload, and the component a fetch case picks is stated by its SHA-256"), ("cache-conformance", "cache rules: a driver, its controls and their reports, with no objects of its own"), ("byte-exact-conformance", "goldens over tests/vectors, whose sidecars state the SHA-256"), -- 2.54.0 (Apple Git-157) From d05ecc7d3359f418d7b20f362c434ab0176dda81 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Wed, 21 Oct 2026 05:27:09 -0400 Subject: [PATCH 4/7] http-delivery receipts: a consumption claim served beside an object is refused for want of its context, placed so every falsify patch still applies Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/src/http_binding/receipts.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/wai/wai-rs/src/http_binding/receipts.rs b/wai/wai-rs/src/http_binding/receipts.rs index cb73b6089..fdbfd76d9 100644 --- a/wai/wai-rs/src/http_binding/receipts.rs +++ b/wai/wai-rs/src/http_binding/receipts.rs @@ -678,12 +678,14 @@ fn step6( .unwrap_or(passed[0].group.signer_pubkey); claim.check_recovery_against(&d, &publisher, matchers).map_err(c) } + // §2.4.13: its objects, the statements that govern them and its room are checked + // together (ai-usage §6.3), never one object at a time. + ClaimBody::Consumption(_) => { + Err(ReceiptRefusal::ContextNotHeld) + } // §2.4.12: its outputs, received objects and envelopes are the sink's, not // served beside an object. ClaimBody::ReconstructSession(_) => Err(ReceiptRefusal::ContextNotHeld), - // §2.4.13: its objects, the statements that governed them and its room are checked - // together (ai-usage §6.3), never one object at a time. - ClaimBody::Consumption(_) => Err(ReceiptRefusal::ContextNotHeld), _ => Ok(()), } } -- 2.54.0 (Apple Git-157) From 5f9473778401bcaa973cbe647c58cfe851561c95 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Wed, 21 Oct 2026 05:32:24 -0400 Subject: [PATCH 5/7] wai.ai.usage: let-chains, a boxed claim in a refusal, and type aliases, for a clippy-clean module Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/src/ai_usage.rs | 38 +++++++++++++++++-------------- wai/wai-rs/src/ai_usage/corpus.rs | 25 ++++++++++++-------- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/wai/wai-rs/src/ai_usage.rs b/wai/wai-rs/src/ai_usage.rs index 426021858..168d2cfd3 100644 --- a/wai/wai-rs/src/ai_usage.rs +++ b/wai/wai-rs/src/ai_usage.rs @@ -645,15 +645,15 @@ pub fn check_fields(signed: Option<&Statement>, fields: &[(String, String)]) -> if reservation.len() > 1 || policy.len() > 1 { return Err(Reason::HeaderConflicts); } - if let Some(v) = reservation.first() { - if trim(v) != r.to_string() { - return Err(Reason::HeaderConflicts); - } + if let Some(v) = reservation.first() + && trim(v) != r.to_string() + { + return Err(Reason::HeaderConflicts); } - if let Some(v) = policy.first() { - if Some(trim(v)) != *p { - return Err(Reason::HeaderConflicts); - } + if let Some(v) = policy.first() + && Some(trim(v)) != *p + { + return Err(Reason::HeaderConflicts); } } } @@ -689,10 +689,10 @@ pub fn governing<'a>(o: &'a HeldObject, room: Option<&'a CheckedRoom>) -> Vec<&' /// §5.2 for one use: `None` when it is outside the vocabulary (`term` is `None`). pub fn preference(entry: &UsageEntry, governing: &[&Statement], terms: Option<&Statement>) -> Option { let term = entry.term.as_deref()?; - if let Some(p) = terms.and_then(|t| t.preference(entry.vocabulary, term)) { - if p != Preference::Unknown { - return Some(p); - } + if let Some(p) = terms.and_then(|t| t.preference(entry.vocabulary, term)) + && p != Preference::Unknown + { + return Some(p); } let ps: Vec = governing.iter().filter_map(|s| s.preference(entry.vocabulary, term)).collect(); Some(if ps.contains(&Preference::Disallow) { @@ -749,11 +749,14 @@ pub struct ClaimContext<'a> { pub previous: Option<&'a ClaimReceipt>, } +/// One object of a claim, by content hash, and what the claim states for it (§6.4). +pub type Judged = ([u8; 32], Outcome); + /// An accepted claim: the claim, and each object's outcome in the order of `objects`. #[derive(Clone, Debug)] pub struct Accepted { pub claim: ClaimReceipt, - pub outcomes: Vec<([u8; 32], Outcome)>, + pub outcomes: Vec, } /// A refused claim: the jwp-receipts §3.1 step, the reason, and the claim where it read. @@ -761,7 +764,7 @@ pub struct Accepted { pub struct Refused { pub step: u8, pub reason: Reason, - pub claim: Option, + pub claim: Option>, } fn body(c: &ClaimReceipt) -> Option<&ConsumptionClaim> { @@ -773,7 +776,8 @@ fn body(c: &ClaimReceipt) -> Option<&ConsumptionClaim> { /// Verify a consumption claim's wire form: jwp-receipts §3.1 steps 1 to 5, then §6.3. pub fn verify_claim(wire: &str, ctx: &ClaimContext<'_>) -> Result { - let refuse = |step: u8, reason: Reason, claim: Option<&ClaimReceipt>| Refused { step, reason, claim: claim.cloned() }; + let refuse = + |step: u8, reason: Reason, claim: Option<&ClaimReceipt>| Refused { step, reason, claim: claim.cloned().map(Box::new) }; let claim = claim_receipt::verify_claim_json(wire).map_err(|r| refuse(r.step, Reason::Claim(r.reason), None))?; claim.check_parent(ctx.previous).map_err(|r| refuse(r.step, Reason::Claim(r.reason), Some(&claim)))?; let Some(b) = body(&claim) else { @@ -843,13 +847,13 @@ pub fn gate( request: &[[u8; 32]], ) -> Result<(), Reason> { // The held claims: the agent's, accepted or refused `usage_disallowed` at step 6. - let mut allowed: Vec<(&ClaimReceipt, &[([u8; 32], Outcome)])> = Vec::new(); + let mut allowed: Vec<(&ClaimReceipt, &[Judged])> = Vec::new(); let mut disallowing: Vec<&ClaimReceipt> = Vec::new(); for c in claims { match c { Ok(a) if a.claim.signer_pubkey == *agent => allowed.push((&a.claim, &a.outcomes)), Err(Refused { step: 6, reason: Reason::UsageDisallowed, claim: Some(c) }) if c.signer_pubkey == *agent => { - disallowing.push(c) + disallowing.push(c.as_ref()) } _ => {} } diff --git a/wai/wai-rs/src/ai_usage/corpus.rs b/wai/wai-rs/src/ai_usage/corpus.rs index 3a8cb60ff..3d23ac466 100644 --- a/wai/wai-rs/src/ai_usage/corpus.rs +++ b/wai/wai-rs/src/ai_usage/corpus.rs @@ -259,7 +259,7 @@ impl Corpus { } /// The objects a claim's context holds, by content hash. - fn held(&self, pubs: &[&str]) -> BTreeMap<[u8; 32], (HeldObject, bool)> { + fn held(&self, pubs: &[&str]) -> Held { let mut m = BTreeMap::new(); for p in pubs { let p = self.publication(p); @@ -588,7 +588,7 @@ impl ClaimSpec { } /// The digests of the governing statements of `objects`, ascending. -fn seen_of(objects: &[[u8; 32]], held: &BTreeMap<[u8; 32], (HeldObject, bool)>, room: Option<&CheckedRoom>) -> Vec<[u8; 32]> { +fn seen_of(objects: &[[u8; 32]], held: &Held, room: Option<&CheckedRoom>) -> Vec<[u8; 32]> { let set: BTreeSet<[u8; 32]> = objects .iter() .filter_map(|h| held.get(h)) @@ -904,10 +904,15 @@ fn object_outcome(case: &Value, groups: &BTreeMap; +/// A claim's name and its verification. +type ClaimResult = (String, Result); + /// The held objects of each publication, by content hash, with whether each is a room's. -fn held_by_publication(doc: &Value) -> Result>, String> { +fn held_by_publication(doc: &Value) -> Result, String> { let gs = groups(doc)?; - let mut out: BTreeMap> = BTreeMap::new(); + let mut out: BTreeMap = BTreeMap::new(); for case in doc["objects"].as_array().ok_or("no objects")? { let p = str_of(case, "publication")?; if let Ok(h) = object_result(case, &gs)? { @@ -955,7 +960,7 @@ fn room_outcome(doc: &Value, case: &Value) -> Result { Ok(v) } -fn response_outcome(case: &Value, held: &BTreeMap>, doc: &Value) -> Result { +fn response_outcome(case: &Value, held: &BTreeMap, doc: &Value) -> Result { let o = object_named(doc, str_of(case, "publication")?, str_of(case, "object")?, held)?; let fields: Vec<(String, String)> = case["fields"] .as_array() @@ -974,7 +979,7 @@ fn object_named<'a>( doc: &Value, publication: &str, name: &str, - held: &'a BTreeMap>, + held: &'a BTreeMap, ) -> Result<&'a HeldObject, String> { let case = doc["objects"] .as_array() @@ -986,7 +991,7 @@ fn object_named<'a>( held.get(publication).and_then(|m| m.get(&h)).map(|x| &x.0).ok_or_else(|| format!("{publication}/{name} is not held")) } -fn catalog_outcome(case: &Value, held: &BTreeMap>, doc: &Value) -> Result { +fn catalog_outcome(case: &Value, held: &BTreeMap, doc: &Value) -> Result { let p = str_of(case, "publication")?; let os = case["objects"] .as_array() @@ -1001,8 +1006,8 @@ fn catalog_outcome(case: &Value, held: &BTreeMap>) -> Result)>, String> { - let mut out: Vec<(String, Result)> = Vec::new(); +fn claim_results(doc: &Value, held: &BTreeMap) -> Result, String> { + let mut out: Vec = Vec::new(); let mut wires: BTreeMap = BTreeMap::new(); for case in doc["claims"].as_array().ok_or("no claims")? { let name = str_of(case, "name")?.to_owned(); @@ -1039,7 +1044,7 @@ fn claim_outcome(r: &Result) -> Value { } } -fn gate_outcome(doc: &Value, case: &Value, claims: &[(String, Result)]) -> Result { +fn gate_outcome(doc: &Value, case: &Value, claims: &[ClaimResult]) -> Result { let p = &case["policy"]; let window = p["window"].as_u64().filter(|w| *w <= MAX_WINDOW as u64).ok_or("a window")? as u8; let policy = Policy { window, require_allowed: p["require_allowed"].as_bool().ok_or("require_allowed")? }; -- 2.54.0 (Apple Git-157) From a7157f691e9a5cd67e736a0cd0f6f58707d83ad7 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Wed, 21 Oct 2026 05:43:49 -0400 Subject: [PATCH 6/7] =?UTF-8?q?ai-usage=20=C2=A74.1:=20a=20manifest=20that?= =?UTF-8?q?=20does=20not=20read=20holds=20no=20statement=20that=20can=20be?= =?UTF-8?q?=20read?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- wai/extensions/ai-usage.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wai/extensions/ai-usage.md b/wai/extensions/ai-usage.md index d1c118f08..66ab1d5ff 100644 --- a/wai/extensions/ai-usage.md +++ b/wai/extensions/ai-usage.md @@ -196,7 +196,8 @@ envelope and its receipts: 1. accepts the receipts as jwp-receipts §3 steps 1 to 5 do, the envelope hashing to the object receipt's `content_hash` excepted; a refusal there is `receipt_invalid`; 2. requires the envelope to hash to that `content_hash`: otherwise `usage_unbound`; -3. reads the manifest's `ai_usage`, where present, by §3.6. +3. reads the manifest's `ai_usage`, where present, by §3.6. A manifest that does not read + (SPEC §2) holds no statement that can be read: `usage_malformed`. An object that passes all three is **held**, with its statement **signed by** the group receipt's `signer_pubkey`, or with none. A publisher that states a preference for an object writes the -- 2.54.0 (Apple Git-157) From bbe32a287a86ee50a40d72424aa7dd474a3ab5da Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Wed, 21 Oct 2026 07:50:26 -0400 Subject: [PATCH 7/7] registry export: regenerated after merging main (live provenance + AI usage) Co-Authored-By: Claude Opus 5.5 --- wai/registry/capabilities.json | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/wai/registry/capabilities.json b/wai/registry/capabilities.json index 0e28a7a91..2de8fcbac 100644 --- a/wai/registry/capabilities.json +++ b/wai/registry/capabilities.json @@ -1,6 +1,20 @@ { "schema": "https://git.transaction.science/Transaction-Science/open-standards/src/branch/main/wai/registry/capabilities.schema.json", "capabilities": [ + { + "capability": "wai.ai.usage", + "kind": "record", + "status": "registered", + "media": null, + "role": null, + "tier": null, + "licence": null, + "replicate": null, + "sets": [], + "spec_section": "§5 Interactive, temporal & computed content", + "edition": null, + "registered": "2026-10-21" + }, { "capability": "wai.asset.provenance", "kind": "record", @@ -1934,6 +1948,13 @@ "section": "§10.5.3", "status": "proposed" }, + { + "name": "WAI-Usage-Claim", + "kind": "http-header", + "document": "extensions/ai-usage.md", + "section": "§7.3", + "status": "proposed" + }, { "name": "WAI1", "kind": "magic", @@ -1969,6 +1990,13 @@ "section": "§3.1", "status": "defined" }, + { + "name": "WAU1", + "kind": "magic", + "document": "extensions/ai-usage.md", + "section": "§4.2", + "status": "defined" + }, { "name": "WCD1", "kind": "magic", @@ -2368,6 +2396,13 @@ "section": "Appendix B", "status": "provisional" }, + { + "name": "wai_ai_usage", + "kind": "catalog", + "document": "extensions/moq-streaming-format.md", + "section": "§4", + "status": "defined" + }, { "name": "wai_c2pa_track", "kind": "catalog", -- 2.54.0 (Apple Git-157)