wai.media.synthesis: origin claims for every kind of media, two marking layers, AI participants, reviews and labels #387
37 changed files with 6911 additions and 48 deletions
27
.github/workflows/wai-conformance.yml
vendored
27
.github/workflows/wai-conformance.yml
vendored
|
|
@ -114,6 +114,10 @@ on:
|
|||
# reports and its §4 the derivations, edge_verify.py reads both tables, and the
|
||||
# registry's wire identifiers test reads its sections.
|
||||
- 'wai/extensions/deriving-edge.md'
|
||||
# tests/synthesis.rs requires this extension's §12 to list the codes the reference
|
||||
# reports and its §10.2 table to pin the reference's icons, synthesis_verify.py reads
|
||||
# both, and the registry's wire identifiers test reads its sections.
|
||||
- 'wai/extensions/synthesis.md'
|
||||
- 'wai/wai-web/**'
|
||||
# The audio provenance check below reads the corpora above with this tool.
|
||||
- 'wai/tools/wai_audio_provenance_check.py'
|
||||
|
|
@ -785,7 +789,8 @@ jobs:
|
|||
# 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), live_ads_verify.py (the live-ads corpus), c2pa_live_verify.py
|
||||
# (the live-provenance corpus) and edge_verify.py (the deriving-edge corpus).
|
||||
# (the live-provenance corpus), edge_verify.py (the deriving-edge corpus) and
|
||||
# synthesis_verify.py (the synthesis 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:
|
||||
|
|
@ -1121,6 +1126,26 @@ jobs:
|
|||
(cd wai-rs && cargo test --no-default-features --features live_provenance --lib live_provenance::)
|
||||
(cd wai-rs && cargo test --no-default-features --features live_provenance --test live_provenance)
|
||||
python3 -I live-conformance/c2pa_live_verify.py live-conformance/c2pa-live
|
||||
# wai.media.synthesis (extensions/synthesis.md): the reference module's unit tests (the
|
||||
# pinned derivations by their rules, the class lattice, a generator's envelope) and
|
||||
# tests/synthesis.rs (the committed corpus regenerates byte for byte from its fixed seeds
|
||||
# and no other file is there, wai-rs gives every case its stated outcome and statements,
|
||||
# §12 lists exactly the reference's codes, each of which the corpus gives, and §10.2's
|
||||
# table pins exactly the reference's icons); then the corpus's second implementation,
|
||||
# synthesis_verify.py, written from the extension's text and sharing no code with wai-rs,
|
||||
# which runs the pinned derivations and writes each digital source type and C2PA
|
||||
# statement itself, verifies each room with live_rooms_verify.py and every record with
|
||||
# WAI's strict Ed25519 rule and BLAKE3 from claim-conformance, reads the extension's icon
|
||||
# and reason tables, and requires every mutated copy of the corpus to fail; standard
|
||||
# library only, run isolated (-I).
|
||||
- name: synthesis provenance · wai-rs and synthesis_verify.py
|
||||
working-directory: wai
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
(cd wai-rs && cargo test --no-default-features --features synthesis --lib synthesis::)
|
||||
(cd wai-rs && cargo test --no-default-features --features synthesis --test synthesis)
|
||||
python3 -I live-conformance/synthesis_verify.py live-conformance/synthesis
|
||||
# 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,
|
||||
|
|
|
|||
|
|
@ -1021,6 +1021,11 @@ impl Grant {
|
|||
return bad;
|
||||
}
|
||||
let Some(attrs) = read_attrs(&attrs) else { return bad };
|
||||
// live-rooms §4.4: `ai`, marking an AI participant, is a model's digest as 64 lowercase
|
||||
// hexadecimal digits.
|
||||
if attrs.iter().any(|(k, v)| k == "ai" && !(v.len() == 64 && v.bytes().all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(&c)))) {
|
||||
return bad;
|
||||
}
|
||||
// Step 6.
|
||||
if exp - nbf > MAX_GRANT_S {
|
||||
return Err(Reason::GrantTooLong);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,59 @@ wire format may change and such changes are recorded here.
|
|||
|
||||
## v0.4.0 — current
|
||||
|
||||
### `wai.media.synthesis`: what any piece of media is, in two marking layers, with reviews and labels
|
||||
|
||||
- **The extension** ([synthesis](extensions/synthesis.md)) registers the record
|
||||
`wai.media.synthesis`. An **origin claim** (`wai-origin`) says of one object, group or track
|
||||
that it is captured, reconstructed (a codec, neural or not, rebuilding what was captured),
|
||||
assistive (an edit that does not substantially alter its input) or synthesized (generated).
|
||||
It names its step (`capture`, `convert`, `assist`, `generate`), the models the step ran, the
|
||||
oversight of a generation, and each input by its content hash and the link hash of its own
|
||||
claim. Its class is the greatest of its step's and its inputs', so it rises along a chain of
|
||||
derivations and never falls; a transcode of a generated video is synthesized. A codec is a
|
||||
conversion: it names no model and keeps its input's class and source, as energy-binding §6.1
|
||||
has a decode.
|
||||
- **IPTC and C2PA as functions of the claim.** The digital source type (`digitalCapture`,
|
||||
`algorithmicallyEnhanced`, `trainedAlgorithmicMedia`, `compositeWithTrainedAlgorithmicMedia`,
|
||||
`compositeSynthetic` and the rest) follows from the step, the inputs, the models and the
|
||||
oversight; the C2PA statement is the actions (`c2pa.created` with the source, or
|
||||
`c2pa.opened` then `c2pa.converted` or `c2pa.edited`) and one `c2pa.ai-disclosure` per trained
|
||||
model, written in JCS. The mapping keeps timed-text's for captions.
|
||||
- **Assistive claims checked, not trusted.** Two integer derivations are pinned: a 2× upscale
|
||||
with `wai.video.int_motion`'s half-sample rounding, and a 3 × 3 binomial denoise. Evidence
|
||||
names the input's and the output's sample digests; a verifier holding the input's decoded
|
||||
samples recomputes the output and refuses a mismatch. A float or unpinned step states
|
||||
`attest` and is reported as asserted; a policy can require proof.
|
||||
- **Two marking layers.** The signed claim is the first; the second is a watermark whose 8-byte
|
||||
payload is the claim's class and the head of its link hash. A marking record names the scheme
|
||||
and the payload embedded, a detection record what a detector read, and a verifier refuses a
|
||||
detection that disagrees with the claim or finds no mark in a marked object, and, under a
|
||||
policy that asks for both layers, a synthesized object with no marking. No watermark scheme is
|
||||
registered: the scheme is a slot a later revision fills.
|
||||
- **AI participants in rooms.** live-rooms §4.4's grant attribute `ai` names the model that acts
|
||||
for a participant; its chat, reactions and captions state origin 2 or 3 with that model, and
|
||||
the room verifier refuses one that does not (`ai_unmarked`, live-rooms §9.2 step 6). Chat and
|
||||
reaction events of live-events gain an optional `origin` and `model` after their last field.
|
||||
Every track an AI participant publishes needs a synthesized track claim naming its model.
|
||||
- **Certified synthesis.** A review (`wai-review`) is a signed verdict over one exact
|
||||
reconstruction at `decode-equivalence` or `generation-equivalence`. A player presents it only
|
||||
where its latest presentation of the object holds that tier on its decode path and output the
|
||||
reconstruction reviewed, so one review holds for every viewer of exact output.
|
||||
- **Labels.** A label policy (`wai-label-policy`) says which classes are labelled, with which
|
||||
visual and audible assets, at what interval and grace; a player's signed log (`wai-player`) of
|
||||
presentations, labels shown, resumptions after a break and reviews presented is held to it:
|
||||
a label at first exposure, after each interruption and within the interval. The EU icons the
|
||||
Code of Practice's Annex 1 publishes are pinned by SHA-256; no audible EU label is published.
|
||||
- **Reference and corpus.** `wai-rs` feature `synthesis` (`src/synthesis.rs`).
|
||||
`live-conformance/synthesis/`, written from fixed seeds by `wai_synthesis_vectors`: a
|
||||
capture's chain through a neural codec and both pinned derivations, a float super-resolution
|
||||
asserted, generated media in both layers, a room with an AI host, a certified and labelled
|
||||
session, and one refused case per reason. Checked by `tests/synthesis.rs` and by
|
||||
`live-conformance/synthesis_verify.py`, a second implementation that shares no code with
|
||||
`wai-rs`, verifies each room with `live_rooms_verify.py`, runs the derivations and writes each
|
||||
statement itself, and requires every mutated copy of the corpus to fail. The registry lists
|
||||
the record and its document kinds.
|
||||
|
||||
### `wai.edge.derivation`: an edge that derives, under its own receipts, chained to its source
|
||||
|
||||
- **The extension** ([deriving-edge](extensions/deriving-edge.md)) registers the record
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ Every registered string has one kind.
|
|||
`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`, `wai.live.ads` and `wai.live.provenance`; timed text,
|
||||
`wai.text.timed`; the deriving edge's record, `wai.edge.derivation`). It names no payload
|
||||
format.
|
||||
`wai.text.timed`; the deriving edge's record, `wai.edge.derivation`; the origin claims of
|
||||
media, `wai.media.synthesis`). 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
|
||||
|
|
@ -209,6 +209,7 @@ and a role are fixed once registered.
|
|||
| `wai.live.room` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content |
|
||||
| `wai.live.steering` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content |
|
||||
| `wai.live.watermark` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content |
|
||||
| `wai.media.synthesis` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content |
|
||||
| `wai.mesh.draco` | payload | registered | volumetric | standalone | royalty-free | standard-unclassified | — | §5 Immersive audio, cinema, volumetric, haptics, HDR |
|
||||
| `wai.mesh.glb` | payload | registered | volumetric | standalone | royalty-free | standard-unclassified | — | §5 Interactive, temporal & computed content |
|
||||
| `wai.meta.dovi` | companion | registered | metadata | companion | proprietary | standard-unclassified | — | §5 Immersive audio, cinema, volumetric, haptics, HDR |
|
||||
|
|
@ -361,11 +362,17 @@ and a role are fixed once registered.
|
|||
| `wai` | catalog | defined | moq-streaming-format §3 |
|
||||
| `wai-c2pa` | catalog | defined | live-provenance §10.2 |
|
||||
| `wai-derivation` | document-kind | defined | deriving-edge §5.1 |
|
||||
| `wai-detection` | document-kind | defined | synthesis §7.3 |
|
||||
| `wai-key-status` | document-kind | defined | jwp-receipts §7.9 |
|
||||
| `wai-label-policy` | document-kind | defined | synthesis §10.1 |
|
||||
| `wai-marking` | document-kind | defined | synthesis §7.2 |
|
||||
| `wai-module-grant` | document-kind | defined | jwp-receipts §7.9 |
|
||||
| `wai-module-grants` | document-kind | defined | http-delivery §8.4 |
|
||||
| `wai-origin` | document-kind | defined | synthesis §3.1 |
|
||||
| `wai-player` | document-kind | defined | synthesis §10.3 |
|
||||
| `wai-receipt-index` | document-kind | defined | http-delivery §8.2 |
|
||||
| `wai-receipts` | catalog | defined | moq-streaming-format §4.1 |
|
||||
| `wai-review` | document-kind | defined | synthesis §9.1 |
|
||||
| `wai1` | codecs-prefix | defined | http-delivery §4.1 |
|
||||
| `wai1` | fourcc | provisional | http-delivery Appendix B |
|
||||
| `wai2` | codecs-prefix | defined | http-delivery §4.1 |
|
||||
|
|
|
|||
27
wai/SPEC.md
27
wai/SPEC.md
|
|
@ -595,8 +595,8 @@ kind:
|
|||
`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`, `wai.live.ads` and `wai.live.provenance`, and timed text,
|
||||
`wai.text.timed`; and an edge's derivation record, `wai.edge.derivation`), which names no
|
||||
payload format.
|
||||
`wai.text.timed`; an edge's derivation record, `wai.edge.derivation`; and the origin claims of
|
||||
media, `wai.media.synthesis`), 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
|
||||
|
|
@ -1118,6 +1118,29 @@ whose proof is an RFC 9942 receipt of inclusion in the repository's RFC 9162 log
|
|||
boxes of each segment ride a provenance track beside the media track:
|
||||
[`extensions/live-provenance.md`](extensions/live-provenance.md).
|
||||
|
||||
And for the **origin** of any media — `wai.media.synthesis`: an origin claim says of one
|
||||
object, one group or one track that it is captured, reconstructed (a codec, neural or not,
|
||||
rebuilding what was captured), assistive (an edit that does not substantially alter its input)
|
||||
or synthesized (generated). It names each input by the link hash of the input's own claim, and
|
||||
its class is the greatest of its step's and its inputs', so the class rises along every chain
|
||||
of derivations and never falls; a generator's own envelope (a `GenerativeDeterministic` or
|
||||
`Behavioral` capability, or `intent` `create`) cannot be claimed as anything else. The claim's
|
||||
IPTC digital source type and its C2PA statement (the actions, and one `c2pa.ai-disclosure`
|
||||
assertion per trained model) are functions of it; a codec's step is a conversion, which asserts
|
||||
no source of its own, as a decode does under energy-binding §6.1. An assistive claim over one
|
||||
of two pinned integer derivations (a 2× upscale and a 3 × 3 denoise) is proved by recomputing
|
||||
the output from the input's decoded samples; any other assistive step can only assert it. The
|
||||
signed claim is the first of two marking layers; the second is a watermark whose payload is the
|
||||
claim's class and the head of its link hash, and a detection that disagrees with the claim is
|
||||
refused (the watermark algorithm is a slot a later revision fills). A room's AI participant,
|
||||
marked in its grant, states its machine origin in its chat, reactions and captions, and labels
|
||||
every track it publishes. A review signed over one exact reconstruction is presented only by a
|
||||
player whose decode path holds the review's tier and whose output is that reconstruction, so one
|
||||
review holds for every viewer of exact output. A label policy says when a label is due (first
|
||||
exposure, after each interruption, at an interval), with the EU icons pinned by digest, and a
|
||||
player's signed log shows whether it was shown:
|
||||
[`extensions/synthesis.md`](extensions/synthesis.md).
|
||||
|
||||
### Payloads of the WAI-native integer codecs
|
||||
|
||||
This revision pins a payload format to each WAI-native integer codec whose row named
|
||||
|
|
|
|||
|
|
@ -176,7 +176,17 @@ producer **MUST NOT** write a summary its message contradicts.
|
|||
|
||||
### 4.4 `chat`
|
||||
|
||||
One text field: the message.
|
||||
| field | size | meaning |
|
||||
|---|---|---|
|
||||
| `text` | text | the message |
|
||||
| `origin` | u8 | optional: 1 human, 2 machine, 3 machine-reviewed |
|
||||
| `model` | 32 | only when `origin` is 2 or 3: a digest naming the model that produced the message |
|
||||
|
||||
`origin` is present exactly when a byte follows `text`; a chat without it states no origin.
|
||||
`origin` and `model` mean what a caption's mean (§4.10): they are the message's
|
||||
machine-generation disclosure, and a sink that labels machine-generated messages takes the
|
||||
label from them. A room may require them of a principal: [live-rooms](live-rooms.md) §4.4
|
||||
marks an AI participant, whose every chat states origin 2 or 3.
|
||||
|
||||
### 4.5 `reaction`
|
||||
|
||||
|
|
@ -186,6 +196,8 @@ One text field: the message.
|
|||
| `value` | u16 | not 0: for emoji and raise, that draft's §4.5 integer encodings; for the others, the application's |
|
||||
| `flags` | u8 | bit 0: a target follows; bit 1: this retracts the reaction it repeats; every other bit 0 |
|
||||
| `target` | 32 | only when bit 0 is set: the digest of the event reacted to |
|
||||
| `origin` | u8 | optional, as a chat's (§4.4): present exactly when a byte follows the field before it |
|
||||
| `model` | 32 | only when `origin` is 2 or 3 |
|
||||
|
||||
A reaction without a target is **standalone**. Standalone reactions are held to the
|
||||
policy's reaction rate (§6.3), as that draft holds them to one per second per device
|
||||
|
|
@ -633,7 +645,9 @@ In a sealed package the events are one component, which
|
|||
forgeries that a lenient Ed25519 verifier accepts, a broken chain, a reorder, a replayed
|
||||
`seq`, `t` going back, an oversize payload, a kind not permitted, both rates exceeded,
|
||||
invalid UTF-8 and overlong text, truncated and malformed records, an unknown version and
|
||||
kind, the wrong room, malformed payloads and a stated tally the replay does not reach.
|
||||
kind, the wrong room, malformed payloads (a chat stating a machine origin without its
|
||||
model among them) and a stated tally the replay does not reach. Chats and a reaction
|
||||
stating each origin have an accepted vector.
|
||||
- **policy cases**: policy records, read (§5) or refused with their reason.
|
||||
- **binding cases**: tags, boxes and MSF records, read back to their record (§8) or
|
||||
refused with their reason.
|
||||
|
|
|
|||
|
|
@ -276,6 +276,20 @@ device captures for, as 64 lowercase hexadecimal digits of its key; and `minor`
|
|||
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.
|
||||
|
||||
One more attribute marks an **AI participant**: `ai`, the digest naming the model that acts
|
||||
for the principal (a prior's digest, SPEC §3.1), as 64 lowercase hexadecimal digits. A grant
|
||||
whose `ai` is not that form breaks this section's rule (§4.3 step 5, `grant_invalid`). The
|
||||
authority states it, as it states `minor`; a principal is an AI participant from the instant
|
||||
a grant carrying it is its grant in effect until one without it is. An AI participant's
|
||||
`chat`, `reaction` and `caption` events state that a machine produced them, with that model
|
||||
(§9.2 step 6), and the tracks it publishes are synthesized media
|
||||
([synthesis](synthesis.md) §8). A relay that forwards a room's events **MUST NOT** forward
|
||||
one that §9.2 step 6 refuses. A sink **MUST** show, with every presence, voice, avatar and
|
||||
message of an AI participant, that it is an AI system: the information Regulation (EU)
|
||||
2024/1689 Article 50(1) asks a system that interacts with people to give. The sink's showing
|
||||
it is an engineering requirement of this extension, and is not a statement about the
|
||||
Regulation.
|
||||
|
||||
### 4.5 How a receipt names a grant
|
||||
|
||||
A receipt that names the grant it was carried under does so with the grant reference
|
||||
|
|
@ -598,6 +612,10 @@ record, in the merged order:
|
|||
5. A `control` event of such a principal needs a power of its grant in effect (§3.3):
|
||||
`record_start` and `record_stop` the record power, any other command the direct power.
|
||||
Otherwise `power_not_granted`.
|
||||
6. A `chat`, `reaction` or `caption` event of a principal whose grant in effect carries
|
||||
`ai` (§4.4) states origin 2 (machine) or 3 (machine-reviewed), with the model `ai`
|
||||
names (live-events §4.4, §4.5, §4.10). One that states no origin, origin 1, or another
|
||||
model is `ai_unmarked`.
|
||||
|
||||
Each principal's effective policy at `t` is so the policy of its grant in effect at `t`,
|
||||
and the room keys' is the authority's. Each record is reported with the grant it was
|
||||
|
|
@ -904,6 +922,7 @@ The codes this extension adds, each with where it is raised:
|
|||
| `participant_limit` | §3.5, §7.2 `join` |
|
||||
| `outside_grant_window` | §4.3, §7.2, §9.2 step 4 |
|
||||
| `power_not_granted` | §9.2 step 5 |
|
||||
| `ai_unmarked` | §4.4, §9.2 step 6 |
|
||||
| `publish_not_permitted` | §7.2 `publish`, `grant_exchange`, `takeover` |
|
||||
| `profile_not_granted` | §7.2 `publish`, `grant_exchange`, `takeover` |
|
||||
| `already_published` | §7.2 `publish` |
|
||||
|
|
@ -953,8 +972,11 @@ key-log head or none, and chains, with the outcome §9.4 gives them.
|
|||
- **Gap**: a chain with a record not delivered.
|
||||
- **Refused**: each reason of §11 at least once, each vector a control of one defect, and
|
||||
the codes of live-events §9 that a descriptor, a grant or a record of a room raises;
|
||||
among them a grant with the seek power whose role lacks it, and a grant and a role with
|
||||
a power above bit 4.
|
||||
among them a grant with the seek power whose role lacks it, a grant and a role with
|
||||
a power above bit 4, a grant whose `ai` attribute does not read, and an AI participant's
|
||||
chat that states no origin, one that states origin 1 and one that names another model.
|
||||
- **AI participant**: a host whose grant names the model that acts for it, its chat and
|
||||
its caption stating that model, beside a person whose chat and reaction state no origin.
|
||||
- **Proofs** (§10.5), in the file's `proofs`: a grant of a room presented with a proof over
|
||||
MOQT (verb `SETUP`) and over HTTP (a WHIP `POST` and a `DELETE`, in their header forms),
|
||||
each accepted; and, each refused with its reason, a grant presented with no proof, a
|
||||
|
|
|
|||
760
wai/extensions/synthesis.md
Normal file
760
wai/extensions/synthesis.md
Normal file
|
|
@ -0,0 +1,760 @@
|
|||
# WAI Extension: Synthesis Provenance
|
||||
|
||||
> Status: Draft. Registers the record `wai.media.synthesis`: what a piece of media is, for every
|
||||
> kind of media WAI carries. An **origin claim** says of one object, one group or one track that
|
||||
> it is **captured**, **reconstructed** (a codec, neural or not, rebuilding what was captured),
|
||||
> **assistive** (an edit that does not substantially alter its input, such as an upscale or a
|
||||
> denoise) or **synthesized** (generated). The claim names each input by the link hash of the
|
||||
> input's own claim, and its class is the greater of the class its own step gives and its
|
||||
> inputs' classes, so the class rises along every chain of derivations and never falls. The
|
||||
> claim restates the content's IPTC digital source type, which is a function of the claim, and
|
||||
> so is its C2PA statement: the actions and the `c2pa.ai-disclosure` assertions a manifest
|
||||
> carries. An assistive claim carries **evidence**: where its step is one of the two integer
|
||||
> derivations this extension pins, a verifier holding the input's decoded samples recomputes the
|
||||
> output and compares it, so the claim that the edit is assistive is checked, not trusted. The
|
||||
> signed claim is the first of two **marking layers**; the second is a watermark whose payload is
|
||||
> a function of the claim, and a verifier refuses a detection that disagrees with it. The
|
||||
> watermark algorithm itself is a slot this revision leaves to a later one. An **AI participant**
|
||||
> of a room is marked in its grant, and its messages, captions and tracks carry its machine
|
||||
> origin. A **review** is a signed verdict over one exact reconstruction, which a player presents
|
||||
> only for a presentation whose decode path holds the review's tier, so one review holds for
|
||||
> every viewer of that output. A **label policy** says when a label must be shown, and a player's
|
||||
> signed **log** shows whether it was. No new transport. The reference implementation is the
|
||||
> `synthesis` module of `wai-rs` (feature `synthesis`); the corpus is
|
||||
> [`live-conformance/synthesis/`](../live-conformance/synthesis/), and its second
|
||||
> implementation is `live-conformance/synthesis_verify.py`.
|
||||
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
|
||||
>
|
||||
> Texts read for this revision, each pinned to the version named: the Code of Practice on
|
||||
> Transparency of AI-Generated Content, final text of 10 June 2026, Section 1 Measures 1.1 to 1.3
|
||||
> and Section 2 Measures 1.1 and 1.2 and its Annex 1; the EU icons the Commission publishes for
|
||||
> that Annex, as fetched on 21 October 2026 from the page it last updated on 13 October 2026
|
||||
> (§10.2); Regulation (EU) 2024/1689, Article 50, paragraphs 2, 4 and 5 as that Code reproduces
|
||||
> them, and the last sentence of paragraph 2 in a reproduction of the Official Journal text (the
|
||||
> Official Journal itself could not be fetched for this revision); the C2PA Technical
|
||||
> Specification, version 2.4, §6.4, §18.15.2, §18.15.4.5 and §18.28; and the IPTC Digital Source
|
||||
> Type vocabulary, whose most recent concept change is dated 2024-10-23. A section number of one
|
||||
> of those texts in this extension is that version's.
|
||||
|
||||
## 1. Scope
|
||||
|
||||
In scope: the records and their encodings (§2); the classes, the steps and the origin claim
|
||||
(§3); the two pinned integer derivations and the evidence that makes an assistive claim
|
||||
checkable (§4); the digital source type (§5) and the C2PA statement (§6) a claim gives; the
|
||||
two marking layers, their records and their agreement (§7); AI participants of rooms (§8); the
|
||||
review and when a player presents it (§9); when a label is shown, the EU icons pinned by digest,
|
||||
and a player's log (§10); verification (§11).
|
||||
|
||||
Out of scope: a transport; how a generator, a codec or an editor works; the watermark algorithm
|
||||
of the second marking layer and its detector, which a later revision registers in the slot of
|
||||
§7.4; how a manifest is assembled, embedded and signed, which is C2PA's and
|
||||
[provenance](provenance.md)'s; key discovery for the signers of these records, which a verifier
|
||||
takes from [jwp-receipts](jwp-receipts.md) §7 as it does for any other key; and the rendering of a
|
||||
label, beyond the asset it shows and the instants it shows it at.
|
||||
|
||||
Relationship to the core spec: `wai.media.synthesis` 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.
|
||||
|
||||
Relationship to [timed-text](timed-text.md) and [live-events](live-events.md): a caption's
|
||||
`origin` (live-events §4.10), and a chat's or a reaction's (§4.4, §4.5 there), is this
|
||||
extension's class for a text event, with no record of its own: origin 1 is a person's text,
|
||||
class `captured` with source `digitalCreation`; origin 2 a machine's, class `synthesized`
|
||||
generated with oversight `fully_autonomous`; origin 3 a machine's reviewed by a person, class
|
||||
`synthesized` with oversight `human_validated`. Timed-text §10.3's export is then §5's and §6's
|
||||
for such a step.
|
||||
|
||||
Relationship to [energy-binding](energy-binding.md) §6.1: a decode, and any codec, reconstructs;
|
||||
it does not create, and a neural codec does not make its output AI-generated. This extension
|
||||
keeps that: a codec is a `convert` step (§3.2), which names no model, asserts no source of its
|
||||
own and keeps its input's class, and its C2PA statement is `c2pa.opened` and `c2pa.converted`
|
||||
with no digital source type (§6).
|
||||
|
||||
Relationship to [determinism-tiers](determinism-tiers.md): a pinned derivation of §4 is integer
|
||||
throughout, so its output is a pure function of its input's samples, and a claim over it is
|
||||
checked by recomputation, at the tier `replicate` promises. A review (§9) is presented only where
|
||||
the presentation holds the review's tier, under the rule of determinism-tiers §2 (*What a tier
|
||||
covers*): a tier covers what the sink emits.
|
||||
|
||||
Duties this extension supports: the two machine-readable marking layers of the Code of Practice
|
||||
(Section 1, Measure 1.1: Sub-measure 1.1.1, signed and time-stamped metadata; Sub-measure 1.1.2,
|
||||
an imperceptible watermark) and its richer provenance information (Measure 1.3: the model, the
|
||||
timestamp, the type of operations); the disclosure of AI systems that interact with people
|
||||
(Article 50(1)); and the placement of a label at first exposure, after interruptions and at
|
||||
regular intervals (Section 2, Sub-measures 1.2.1 to 1.2.3). WAI states what its records carry and
|
||||
what a verifier checks. It makes no statement about any regime, and a deployment confirms what a
|
||||
regime asks of it against that regime's text.
|
||||
|
||||
## 2. Records
|
||||
|
||||
Every record of this extension is a JSON object (RFC 8259) under the encodings of
|
||||
[jwp-receipts](jwp-receipts.md) §2.4.3: byte strings in lowercase hexadecimal; integers unsigned,
|
||||
at most 2^53 − 1, in plain decimal; no other number; no boolean; a **term** and a **date-time**
|
||||
as defined there. A record is read as jwp-receipts §3.1 step 1 reads a claim: RFC 8259's grammar,
|
||||
no member named twice in one object, at most 32 levels of nesting. Each record has exactly the
|
||||
members its section lists, no member missing and none added, at any level, and a `kind` naming
|
||||
it; one that does not is `record_malformed`.
|
||||
|
||||
Every record carries `signer_pubkey` (32 bytes, the Ed25519 key of RFC 8032 that signs it) and
|
||||
`sig` (64 bytes):
|
||||
|
||||
```
|
||||
sig = Ed25519(signer_key, "wai:synthesis\x01" || JCS(record without "sig"))
|
||||
link_hash = BLAKE3("wai:synthesis-link\x01" || JCS(record))
|
||||
```
|
||||
|
||||
JCS is RFC 8785, which on these values reduces to what jwp-receipts §2.4.3 states. The signature
|
||||
is verified strictly ([SPEC](../SPEC.md) §7 *Signatures*): a key that fails step 1 or 2 is
|
||||
`bad_key`, and a signature that fails a later step `bad_signature`. A record is named by its
|
||||
link hash wherever another names it. A record whose members carry `issued_at` holds a date-time
|
||||
there, the instant its signer signed it: the time stamp the Code of Practice's Sub-measure 1.1.1
|
||||
asks for, self-asserted.
|
||||
|
||||
A digest of samples is SHA-256 (FIPS 180-4) over a canonical buffer: for an image, the `RGB8`
|
||||
buffer of [reconstruction-binding](reconstruction-binding.md) §2 (row-major, three bytes a
|
||||
pixel, no row padding); for another medium, the canonical buffer its capability's
|
||||
`*-equivalence` names ([determinism-tiers](determinism-tiers.md) §2). An object's **content
|
||||
hash** is jwp-receipts §2.1's.
|
||||
|
||||
## 3. Classes and the origin claim
|
||||
|
||||
### 3.1 The origin claim
|
||||
|
||||
An origin claim, `kind` `"wai-origin"`, has these members:
|
||||
|
||||
| member | type | meaning |
|
||||
|---|---|---|
|
||||
| `kind` | string | `"wai-origin"` |
|
||||
| `subject` | object | what it is about: `{"type": "object", "content_hash"}`, an object by its content hash; `{"type": "group", "receipt"}`, a group by its group receipt's link hash (jwp-receipts §3 step 3); or `{"type": "track", "track", "from_group"}`, a track by its digest (live-rooms §6.2: SHA-256 over the rendering of its namespace, one zero byte and its name) from the group `from_group` on |
|
||||
| `media` | string | `audio`, `video`, `image` or `text` |
|
||||
| `class` | string | `captured`, `reconstructed`, `assistive` or `synthesized` (§3.2) |
|
||||
| `step` | string | `capture`, `convert`, `assist` or `generate` (§3.2) |
|
||||
| `operation` | term | what ran: a capability or derivation string, or a term naming the type of operation (`upscale`, `inpainting`, `speech-synthesis`) |
|
||||
| `record` | 32 B hex or `null` | the identity of a record that derived the subject, where its deriver signs one; `null` otherwise |
|
||||
| `source` | string | the subject's digital source type (§5) |
|
||||
| `models` | array | the models the step ran, at most 16, each `{"digest", "type"}`: a prior's digest (SPEC §3.1) and `trained` (a model derived from sampled content) or `algorithmic` (one that is not); no digest twice |
|
||||
| `oversight` | string or `null` | for `generate`: `fully_autonomous`, `prompt_guided` or `human_validated` (C2PA 2.4 §18.28.4); otherwise `null` |
|
||||
| `inputs` | array | the objects the step read, at most 16, each `{"content_hash", "origin", "class", "source"}`: its content hash, the link hash of its own origin claim or `null` where it has none, and that claim's class and source, restated; no content hash twice |
|
||||
| `evidence` | object or `null` | for `assist`: §4.3; otherwise `null` |
|
||||
| `issued_at` | date-time | when the signer signed it |
|
||||
| `signer_pubkey`, `sig` | | §2 |
|
||||
|
||||
`media`, `class`, `step`, `oversight`, a model's `type` and an input's `class` hold only the
|
||||
values the table names, and `source` and an input's `source` only the codes of §5. Otherwise
|
||||
`record_malformed`.
|
||||
|
||||
Who signs: the party that performed the step: the capturing device or its operator, the
|
||||
encoder, the editor, the generator's provider. A claim carries its signer's authority and no
|
||||
more; which signers a verifier trusts is its policy.
|
||||
|
||||
A verifier is given at most one origin claim for one subject; a second for a subject an earlier
|
||||
claim names is `claim_conflict`.
|
||||
|
||||
### 3.2 Steps and the class lattice
|
||||
|
||||
The classes are ordered: `captured` < `reconstructed` < `assistive` < `synthesized`. Each step
|
||||
gives a class, and constrains the claim:
|
||||
|
||||
| step | class it gives | inputs | models | `oversight` | `evidence` |
|
||||
|---|---|---|---|---|---|
|
||||
| `capture` | `captured` | none | none | `null` | `null` |
|
||||
| `convert` | `reconstructed` | at least one | none | `null` | `null` |
|
||||
| `assist` | `assistive` | exactly one | any | `null` | an object |
|
||||
| `generate` | `synthesized` | any | at least one | not `null` | `null` |
|
||||
|
||||
A `capture` states as its `source` one of `digitalCapture`, `computationalCapture`,
|
||||
`screenCapture` or `digitalCreation`: a recording of the world, or a person's work made without
|
||||
a generative model. A claim that breaks the table is `record_malformed`.
|
||||
|
||||
- **capture** records the world, or a person's authorship: a camera, a microphone, a screen
|
||||
recording, a person typing or drawing.
|
||||
- **convert** rebuilds or rearranges its inputs without generating or enhancing anything: an
|
||||
encode, a decode, a transcode, a re-package, a decoder's reconstruction (neural or not), and a
|
||||
derivation that copies samples or composes them (crop, rotation, grid or overlay composition,
|
||||
[derived-tracks](derived-tracks.md)). A codec names no model: its parameter set is pinned in
|
||||
its envelope, and it reconstructs what its input holds.
|
||||
- **assist** edits one input without substantially altering it: an upscale, a denoise, a colour
|
||||
correction. It is the step whose claim needs evidence (§4).
|
||||
- **generate** produces content by a model: a generated picture, voice or video, an inpainting,
|
||||
a voice clone, a talking head, a frame generated between two others, an avatar a model drives.
|
||||
|
||||
The **class of a claim** is the greatest of its step's class and its inputs' stated classes. A
|
||||
claim whose `class` is another is `class_mismatch`. So a conversion of a synthesized input is
|
||||
synthesized, and a transcode, a clip or a re-package can never lower what its input was.
|
||||
|
||||
### 3.3 Checks against what a verifier holds
|
||||
|
||||
- **Inputs.** For each input whose `origin` names a claim the verifier holds: that claim's
|
||||
subject is the object of the input's `content_hash`, and its `class` and `source` are the
|
||||
input's. Otherwise `input_mismatch`. An input whose claim is not held is taken as stated.
|
||||
- **The subject's own envelope.** Where the subject is an object and the verifier holds its
|
||||
envelope (SPEC §2): an envelope whose capability's registry class is `GenerativeDeterministic`
|
||||
or `Behavioral` (SPEC §7), or whose manifest's `intent` is `create`, is generated by every sink
|
||||
that presents it, and its claim's `step` is `generate`. A claim of another step over such an
|
||||
envelope is `class_understated`.
|
||||
|
||||
### 3.4 Through every derivation
|
||||
|
||||
A party that derives an object from others and publishes it signs an origin claim for it whose
|
||||
inputs name the inputs' claims; where it signs a record of the derivation too, the claim's
|
||||
`record` names that record. A relay forwards claims as jwp-receipts §4.1 has it forward any
|
||||
receipt, byte for byte, and alters none. So each object a sink receives has a claim, and the
|
||||
chain of claims behind it reaches the capture or generation it came from:
|
||||
|
||||
| derivation | the output's claim |
|
||||
|---|---|
|
||||
| a relay forwards an object | none: the object is the same, and its claim holds for it |
|
||||
| an object re-packaged, or a `WAI2` component exploded (SPEC §7.1) | the same object for every rule that names objects by content hash; its claim holds |
|
||||
| an encode, a transcode, a rung of a ladder, a decoder's reconstruction | `convert` of the input |
|
||||
| a crop, a rotation, a composition of several inputs | `convert` of the inputs |
|
||||
| an upscale, a denoise, a sink-side enhancement | `assist` of the input, with evidence (§4) |
|
||||
| a frame generated between two others, a talking head from a picture and a voice, an inpainting | `generate` over the inputs |
|
||||
| a clip of a recording, a segment of a track | the claim of each group or object it holds, unchanged |
|
||||
|
||||
A track or group claim covers each object of the track or group; an object claim, where one is
|
||||
given, is the object's.
|
||||
|
||||
## 4. Assistive evidence
|
||||
|
||||
### 4.1 `wai-synthesis:upscale2`
|
||||
|
||||
The derivation `wai-synthesis:upscale2`, with no parameters (`{}`), maps a *w* × *h* `RGB8` buffer
|
||||
`in` to a 2*w* × 2*h* buffer `out`. For each pixel (*x*, *y*) of `in` and each of its three bytes,
|
||||
with *x*₁ = min(*x* + 1, *w* − 1), *y*₁ = min(*y* + 1, *h* − 1), *a* = in(*x*, *y*),
|
||||
*b* = in(*x*₁, *y*), *c* = in(*x*, *y*₁) and *d* = in(*x*₁, *y*₁):
|
||||
|
||||
- out(2*x*, 2*y*) = *a*;
|
||||
- out(2*x* + 1, 2*y*) = (*a* + *b* + 1) >> 1;
|
||||
- out(2*x*, 2*y* + 1) = (*a* + *c* + 1) >> 1;
|
||||
- out(2*x* + 1, 2*y* + 1) = (*a* + *b* + *c* + *d* + 2) >> 2.
|
||||
|
||||
This is the half-sample interpolation `wai.video.int_motion` predicts with (SPEC §5), in the same
|
||||
integer arithmetic.
|
||||
|
||||
### 4.2 `wai-synthesis:denoise3`
|
||||
|
||||
The derivation `wai-synthesis:denoise3`, with no parameters (`{}`), maps a *w* × *h* `RGB8`
|
||||
buffer `in` to a *w* × *h* buffer `out`: for each pixel (*x*, *y*) and each of its three bytes,
|
||||
|
||||
out(*x*, *y*) = (8 + Σ *k*(*i*, *j*) · in(clamp(*x* + *i*), clamp(*y* + *j*))) >> 4,
|
||||
|
||||
the sum over *i*, *j* ∈ {−1, 0, 1}, with *k* = 4 at (0, 0), 2 where exactly one of *i*, *j* is 0,
|
||||
and 1 at the four corners; clamp keeps a coordinate within 0 to *w* − 1 (or *h* − 1).
|
||||
|
||||
### 4.3 The evidence
|
||||
|
||||
An assist claim's `evidence` is an object of exactly these members:
|
||||
|
||||
| member | type | meaning |
|
||||
|---|---|---|
|
||||
| `tier` | string | `replicate`: the step is a pinned derivation, checkable by recomputation; `attest`: it is not, and the claim asserts that the edit is assistive |
|
||||
| `derivation` | term | the derivation run |
|
||||
| `params` | object | its parameters |
|
||||
| `width`, `height` | integer | the input's size, each at least 1 |
|
||||
| `input_samples` | 32 B hex | the digest of the input's samples (§2) |
|
||||
| `output_samples` | 32 B hex | the digest of the output's samples |
|
||||
|
||||
`tier` and the sizes outside these values are `record_malformed`. `replicate` evidence names
|
||||
`wai-synthesis:upscale2` or `wai-synthesis:denoise3`, with `params` `{}`, and an output of at most
|
||||
2^26 bytes (4 · `width` · `height` · 3 for the upscale, `width` · `height` · 3 for the denoise);
|
||||
otherwise `evidence_invalid`. A derivation evaluated in float, or by any operation this
|
||||
revision does not pin, states `attest`: the edit may well be assistive, but its claim can only
|
||||
assert it.
|
||||
|
||||
A model-driven derivation joins the pinned set when a capability registers it in integer
|
||||
arithmetic at `decode-equivalence`, with its parameter set pinned (SPEC §3.1); its evidence then
|
||||
names it and its pin, and is checked the same way. This revision registers none.
|
||||
|
||||
### 4.4 Checking the evidence
|
||||
|
||||
A verifier is given the decoded samples of the objects it holds (§11.1). For an assist claim, in
|
||||
this order:
|
||||
|
||||
1. `replicate` evidence that §4.3 does not admit: `evidence_invalid`.
|
||||
2. With `replicate` evidence and the input's decoded samples held: they are `width` · `height` ·
|
||||
3 bytes and their digest is `input_samples` (`input_samples_mismatch`); the derivation is run
|
||||
over them, and the digest of its output is `output_samples`
|
||||
(`assistive_recompute_mismatch`). The claim is then **proved**. Without the input's samples it
|
||||
is **not recomputed**; `attest` evidence is **asserted**.
|
||||
3. With the subject's decoded samples held (an object subject): their digest is
|
||||
`output_samples` (`samples_mismatch`).
|
||||
4. Under a policy that requires proof (§11.1), a claim that is not proved:
|
||||
`assistive_not_proved`.
|
||||
|
||||
A proved claim says that this output is that pinned derivation of that input, and nothing else:
|
||||
a fact any verifier holding the input's samples checks, at the tier `replicate` promises.
|
||||
|
||||
## 5. The digital source type
|
||||
|
||||
A claim's `source` is a code of the IPTC Digital Source Type vocabulary, whose terms are
|
||||
`http://cv.iptc.org/newscodes/digitalsourcetype/` followed by the code. It is a function of the
|
||||
claim:
|
||||
|
||||
| step | inputs | `source` |
|
||||
|---|---|---|
|
||||
| `capture` | none | as stated: `digitalCapture`, `computationalCapture`, `screenCapture` or `digitalCreation` |
|
||||
| `convert` | one | the input's `source` |
|
||||
| `convert` | several | `compositeSynthetic` when an input's class is `synthesized`; else `compositeCapture` when every input's source is `digitalCapture` or `computationalCapture`; else `composite` |
|
||||
| `assist` | one | the input's `source` when its class is `synthesized`; else `algorithmicallyEnhanced` |
|
||||
| `generate` | any | the **generated source**: `algorithmicMedia` when no model is `trained`; else `compositeWithTrainedAlgorithmicMedia` when `oversight` is `human_validated` or an input's class is not `synthesized`; else `trainedAlgorithmicMedia` |
|
||||
|
||||
A claim whose `source` is another is `source_mismatch`. The codes are the vocabulary's own
|
||||
definitions: `algorithmicallyEnhanced` is an algorithmic modification that leaves the main
|
||||
content unchanged; `trainedAlgorithmicMedia` media created by a model trained on captured
|
||||
content; `compositeWithTrainedAlgorithmicMedia` an edit made with such a model, and, by C2PA 2.4
|
||||
§18.28.3's pairing, generated content a person reviewed and approved; `algorithmicMedia` media
|
||||
created purely by an algorithm with no sampled training data. A `reconstructed` claim keeps its
|
||||
input's code: a codec makes no claim about what it reconstructed.
|
||||
|
||||
## 6. The C2PA statement
|
||||
|
||||
A claim gives one statement, the JSON object `{"actions": A, "assertions": D}` written in JCS
|
||||
(RFC 8785):
|
||||
|
||||
- `A`, the actions of the manifest's first actions assertion (C2PA 2.4 §18.15.2):
|
||||
- a claim with no input or with several: `[{"action": "c2pa.created", "digitalSourceType": U}]`,
|
||||
`U` the IPTC term of the claim's `source`;
|
||||
- a `convert` of one input: `[{"action": "c2pa.opened"}, {"action": "c2pa.converted"}]`, with
|
||||
no digital source type, as [energy-binding](energy-binding.md) §6.1 has it for a decode: the
|
||||
input is the parent ingredient, and its own manifest states its source;
|
||||
- an `assist` of one input: `[{"action": "c2pa.opened"}, {"action": "c2pa.edited",
|
||||
"digitalSourceType": U}]`, `U` the term of `algorithmicallyEnhanced`;
|
||||
- a `generate` of one input: the same, `U` the term of the generated source (§5).
|
||||
- `D`, one `c2pa.ai-disclosure` assertion (C2PA 2.4 §18.28) for each model whose `type` is
|
||||
`trained`, in the claim's order, labelled `c2pa.ai-disclosure`, `c2pa.ai-disclosure__1`, …
|
||||
(C2PA 2.4 §6.4): `{"label": L, "data": {"modelType": "c2pa.types.model", "modelIdentifier":
|
||||
"tag:transaction.science,2026:wai/model/" ‖ the digest in lowercase hexadecimal}}`, and, for a
|
||||
`generate`, `"contentProfile": {"humanOversightLevel": O}` in `data`, `O` the claim's
|
||||
`oversight`. A model of type `algorithmic` has none: C2PA 2.4 §18.28.3 attaches the assertion
|
||||
only where a trained model was invoked. The model is named by digest, as timed-text §10.3 names
|
||||
a caption's.
|
||||
|
||||
Embedding the statement in a manifest, and signing that, is C2PA's and provenance's. A rendition
|
||||
of a playlist whose objects include one of class `synthesized` SHOULD carry the
|
||||
`public.machine-generated` characteristic (`draft-pantos-hls-rfc8216bis-22` §4.4.6.1), as
|
||||
timed-text §10.2 has it for captions.
|
||||
|
||||
## 7. Two marking layers
|
||||
|
||||
### 7.1 The layers
|
||||
|
||||
The **first layer** is the origin claim: signed, time-stamped metadata (the Code of Practice's
|
||||
Sub-measure 1.1.1). The **second** is an imperceptible watermark embedded in the content
|
||||
(Sub-measure 1.1.2), whose payload is a function of the claim (§7.2). The Code asks for both on
|
||||
the outputs of generative systems and treats fingerprinting or logging as a supplement that is
|
||||
never sufficient alone (Sub-measure 1.1.3); a content-bound commitment, such as a
|
||||
[reconstruction binding](reconstruction-binding.md) over decoded pixels, is a supplement of that
|
||||
kind here and not the second layer.
|
||||
|
||||
Under a policy that requires both layers (§11.1), an object claim of class `synthesized` with no
|
||||
marking record naming it (§7.2) is `layer_missing`.
|
||||
|
||||
### 7.2 The payload and the marking record
|
||||
|
||||
A watermark carrying a claim holds an 8-byte **payload**: the code of the claim's class (0
|
||||
`captured`, 1 `reconstructed`, 2 `assistive`, 3 `synthesized`), then the first seven bytes of
|
||||
the claim's link hash. Read from the content alone, with no metadata, it says the class and points
|
||||
at the claim. Seven bytes find a claim; they do not prove one, and the claim's signature does
|
||||
that. The marking record names the scheme that reads it (§7.4); a signpost in the content naming
|
||||
the detection solution, as the Code's Measure 3.4(c)(ii) describes one, is not part of this
|
||||
revision.
|
||||
|
||||
The party that embeds the mark signs a **marking** record, `kind` `"wai-marking"`:
|
||||
|
||||
| member | type | meaning |
|
||||
|---|---|---|
|
||||
| `kind` | string | `"wai-marking"` |
|
||||
| `subject` | 32 B hex | the content hash of the marked object |
|
||||
| `origin` | 32 B hex | the link hash of its origin claim |
|
||||
| `scheme` | term | the watermark scheme (§7.4) |
|
||||
| `scheme_digest` | 32 B hex | the SHA-256 of the scheme's definition |
|
||||
| `payload` | 8 B hex | the payload embedded |
|
||||
| `issued_at` | date-time | when it signed it |
|
||||
| `signer_pubkey`, `sig` | | §2 |
|
||||
|
||||
A verifier holds the claim the marking names (`claim_unknown`); that claim's subject is the
|
||||
marking's object, and the marking's payload is the claim's (§7.2) (`marking_mismatch`).
|
||||
|
||||
### 7.3 Detection and agreement
|
||||
|
||||
A detector that reads a watermark signs a **detection** record, `kind` `"wai-detection"`: the
|
||||
members of a marking but `origin`, with `payload` the 8 bytes read, or `null` where it found no
|
||||
mark. For each detection, in order, with the origin claim whose subject is the detection's
|
||||
object (if the verifier holds one) and the first marking whose subject it is (if it holds one):
|
||||
|
||||
1. With a marking: the detection's `scheme` and `scheme_digest` are the marking's
|
||||
(`detection_mismatch`).
|
||||
2. With no payload read: where a marking says the object was marked, `watermark_absent`, the
|
||||
second layer having been removed; otherwise the object is **unmarked**.
|
||||
3. With a payload read and a claim held: the payload is the claim's (§7.2); otherwise
|
||||
`layers_disagree`. A content that carries the mark of a synthesized claim while its metadata
|
||||
says captured, or the mark of another claim, is refused here. With no claim held, the
|
||||
detection is **not checked**.
|
||||
|
||||
### 7.4 The scheme slot
|
||||
|
||||
This revision registers no watermark scheme. A scheme is named by a term `wai-mark:<name>` and
|
||||
the digest of its definition, which a later revision pins with its embedding as an integer
|
||||
derivation appended to a generative one, so that a sink that generates content embeds the mark
|
||||
as part of the reconstruction it is held to, and with its key-free detector. Until then a
|
||||
detection is a signed statement by its detector and nothing more. The conformance corpus names
|
||||
`wai-mark:conformance`, whose definition is the text `wai-mark:conformance names no watermark
|
||||
algorithm (synthesis.md section 7.4)` and which names no algorithm: it exercises the records and
|
||||
their agreement, not a watermark.
|
||||
|
||||
## 8. Live rooms
|
||||
|
||||
### 8.1 AI participants and their events
|
||||
|
||||
[live-rooms](live-rooms.md) §4.4 marks an **AI participant**: its grant's `ai` attribute names the
|
||||
model that acts for it. Its `chat`, `reaction` and `caption` events state origin 2 or 3 with that
|
||||
model ([live-events](live-events.md) §4.4, §4.5, §4.10); a room verifier refuses one that does
|
||||
not, `ai_unmarked` (live-rooms §9.2 step 6), and a relay does not forward it.
|
||||
|
||||
### 8.2 Its tracks
|
||||
|
||||
Every track an AI participant publishes is synthesized media. Its publisher **MUST** sign, for
|
||||
each layer track it publishes, a track claim (§3.1, `subject` of type `track`) of class
|
||||
`synthesized`, step `generate`, whose `models` include the model the grant's `ai` names; the
|
||||
claim is signed by the participant's key or a room key. A relay **MUST NOT** forward the objects
|
||||
of such a track while it holds no such claim.
|
||||
|
||||
A verifier given a room (§11.1) replays its lifecycle events in the merged order (live-rooms
|
||||
§9.2): each `join` and `grant_exchange` sets its subject's grant, each `leave` clears it, and for
|
||||
each `publish` and `takeover` whose subject's grant names a model in `ai`, each layer track
|
||||
*k* from 0 to `layers` − 1, owned by the subject (its key in lowercase hexadecimal) or by the slot
|
||||
(`_` and its name), of the published media kind, is named by its digest (live-rooms §6.2). A track
|
||||
with no claim as above is `ai_track_unlabelled`, at that lifecycle event.
|
||||
|
||||
## 9. Certified synthesis
|
||||
|
||||
### 9.1 The review
|
||||
|
||||
A person or an organisation that reviews a synthesized output signs a **review**, `kind`
|
||||
`"wai-review"`:
|
||||
|
||||
| member | type | meaning |
|
||||
|---|---|---|
|
||||
| `kind` | string | `"wai-review"` |
|
||||
| `subject` | 32 B hex | the content hash of the object reviewed |
|
||||
| `origin` | 32 B hex | the link hash of its origin claim, of class `synthesized` |
|
||||
| `tier` | string | the tier at which the reconstruction reviewed is portable: `decode-equivalence` or `generation-equivalence` ([determinism-tiers](determinism-tiers.md) §2; SPEC §7 `GenerativeDeterministic`) |
|
||||
| `reconstruction` | 32 B hex | the digest of the canonical output the reviewer saw (§2) |
|
||||
| `decoder` | term | the decoder the reviewer's output came from |
|
||||
| `variant` | term or `null` | its decode path, where it has more than one |
|
||||
| `scope` | string | what was reviewed: `accuracy` (words, a script), `rendering` (pixels, a voice, signing), `translation` |
|
||||
| `verdict` | string | `approved`, `approved-with-notes` or `rejected` |
|
||||
| `notes` | 32 B hex or `null` | the digest of the notes, `null` exactly for `approved` |
|
||||
| `role` | term or `null` | a role the reviewer holds for this scope, as the reviewer states it |
|
||||
| `issued_at` | date-time | when it signed it |
|
||||
| `signer_pubkey`, `sig` | | §2 |
|
||||
|
||||
A verifier holds the claim a review names (`claim_unknown`), whose subject is the review's object
|
||||
and whose class is `synthesized` (`review_subject_mismatch`).
|
||||
|
||||
### 9.2 Presenting a review
|
||||
|
||||
A player **proves its tier** by stating its decode path and the digest of what it presented, in a
|
||||
`present` entry of its log (§10.3): the decoder, its variant, the tier the path holds, and the
|
||||
output's digest. The tier a path holds is the weakest along it: the decoder's tier at its pin
|
||||
(SPEC §3.1), and `none` where any float stage, or any processing after the decode that alters the
|
||||
medium, lies on the path ([determinism-tiers](determinism-tiers.md) §2 *What a tier covers*: a
|
||||
tier covers what the sink emits). A verifier that holds the object's decoded samples (the output
|
||||
of a decode at the stated tier) checks the stated digest against them:
|
||||
`present_output_irreproducible` where it differs.
|
||||
|
||||
A player **MAY** present a review, in a `review` entry of its log, only for an object it has
|
||||
presented, and only where its latest presentation of the object before that entry:
|
||||
|
||||
1. presented the review's object, which the verifier holds the review of
|
||||
(`claim_unknown`, `review_subject_mismatch`);
|
||||
2. under a review whose verdict is not `rejected` (`review_rejected`);
|
||||
3. on a path of the review's tier (`review_path_not_exact`): a path of tier `none` or
|
||||
`entropy-consistency` never presents a review made at `decode-equivalence`;
|
||||
4. with the review's `reconstruction` as its output's digest (`review_reconstruction_mismatch`).
|
||||
|
||||
Where these hold, the viewer received the output the reviewer saw, byte for byte: one review
|
||||
holds for every viewer whose presentation is exact at that tier, and for no other.
|
||||
|
||||
## 10. Label display
|
||||
|
||||
### 10.1 When a label is shown
|
||||
|
||||
A **label policy**, `kind` `"wai-label-policy"`, signed by the party that presents content to
|
||||
viewers, says which content is labelled and how:
|
||||
|
||||
| member | type | meaning |
|
||||
|---|---|---|
|
||||
| `kind` | string | `"wai-label-policy"` |
|
||||
| `classes` | array | the classes labelled, 1 to 4 of §3.2's, in its order, none twice |
|
||||
| `visual` | 32 B hex | the SHA-256 of the visual label shown with visual media |
|
||||
| `audio` | 32 B hex or `null` | the SHA-256 of the audible label played with audio-only media; `null` for none |
|
||||
| `interval_us` | integer or `null` | the longest a labelled presentation goes without a label, in microseconds of the log's clock; at least 1 when not `null` |
|
||||
| `grace_us` | integer | how long after the instant it is due a label may come |
|
||||
| `issued_at` | date-time | when it signed it |
|
||||
| `signer_pubkey`, `sig` | | §2 |
|
||||
|
||||
A label is due, for each kind (visual for `video`, `image` and `text`, audible for `audio`):
|
||||
|
||||
- at **first exposure**: the first presentation of labelled content of that kind in the session
|
||||
(the Code of Practice's Sub-measure 1.2.1(d));
|
||||
- after each **interruption**: the first presentation of labelled content of that kind after the
|
||||
presentation resumed from an ad break or another interruption (Sub-measure 1.2.2(b); live-ads'
|
||||
breaks end on a `cue` event, live-events §4.3);
|
||||
- at **intervals**: where the policy has one, every presentation of labelled content of that kind
|
||||
that is neither, no longer than the interval after the last label (Sub-measures 1.2.2(b) and
|
||||
1.2.3).
|
||||
|
||||
### 10.2 The EU icons, pinned
|
||||
|
||||
The Code of Practice's Annex 1 publishes three EU icons, for content fully generated by AI
|
||||
(`AI GENERATED`), for content partially modified by AI (`AI MODIFIED`), and a basic one (`AI`),
|
||||
each in four variations. The Commission publishes them as two archives: SVG
|
||||
(`https://ec.europa.eu/newsroom/dae/redirection/document/129546`, SHA-256
|
||||
`f76707b69b546677b173975c018528e5e549e12afb05550f32c2f46500ffbefd` as fetched) and PNG
|
||||
(`https://ec.europa.eu/newsroom/dae/redirection/document/129547`, SHA-256
|
||||
`aedc51656497a2e8f3e42798e5749a929952f5b262bad368ee3057f58c0f52c7` as fetched). Each file is
|
||||
pinned here by the SHA-256 of its bytes:
|
||||
|
||||
| file | SHA-256 |
|
||||
|---|---|
|
||||
| `LABEL_AI GENERATED_black transparent.svg` | `63d28ab55916b4548edff21d5fbcac065a13e5fb936e9b304ff0bada45f2fac1` |
|
||||
| `LABEL_AI GENERATED_black.svg` | `503af176b05fd725e68b0aa526977d31bcd657d74b15c6103a57ace81384940f` |
|
||||
| `LABEL_AI GENERATED_white transparent.svg` | `d19736f6da9d38af3cffce8ba08f3f7658b573e1794c173fcd8b55a5c200ed44` |
|
||||
| `LABEL_AI GENERATED_white.svg` | `10125cdef3fc60a351df72fe1266bb00ea046086921bd1bb20135f0f07b5ed36` |
|
||||
| `LABEL_AI MODIFIED_black transparent.svg` | `9ab09f54c1ccef01799af43794cd93a8af6607ba9ac6b9526fb90a02be8ddf3d` |
|
||||
| `LABEL_AI MODIFIED_black.svg` | `2e7349e5eca4ee78eeef160dfef4545c31850932245373a07c5401e73e6599c0` |
|
||||
| `LABEL_AI MODIFIED_white transparent.svg` | `7ccbce41f9f821a574007b32806ddb6d635ee8bbc5d753ccd05f66f5a3165845` |
|
||||
| `LABEL_AI MODIFIED_white.svg` | `0c72a7d569a1447e2982a843e156bd5f9cb2ab6f201943c6c68f988dc2f1bb6e` |
|
||||
| `LABEL_AI_black transparent.svg` | `f8e20f1dde8c7d95940da08960b1063620812acc9b4ea96eb5ad051f80f67c21` |
|
||||
| `LABEL_AI_black.svg` | `58ffe859a4d74829d397f534a988081bcefb716849c7d3b84fa7a026dd1d257f` |
|
||||
| `LABEL_AI_white transparent.svg` | `52d87cb3f6a191dba24796f745e1eeb80d342883acd1a2e6d856342ad853766c` |
|
||||
| `LABEL_AI_white.svg` | `5b3b94ae67fea55c4f2d013d0b8ed5b876c1533555ed0ed60e688f2f3ccf1a50` |
|
||||
| `LABEL_AI GENERATED_black transparent.png` | `74c8c4ee720c4a3286c3c1b9f216ac9b918ebf449e8c421dde5a40cd216b27e6` |
|
||||
| `LABEL_AI GENERATED_black.png` | `8d0af57bc93ba3797042a4b56db757d85f924d9b0200b75b8842da7be5e402a2` |
|
||||
| `LABEL_AI GENERATED_white transparent.png` | `06c4c7c00738e0e44c35f5927e72842ff7a9dd757e0374335b9cc1ba6bb52199` |
|
||||
| `LABEL_AI GENERATED_white.png` | `f139d8de4b5173f47a777431e7e03d9258d8ea7ee1ee1a062febc7e9fff067fc` |
|
||||
| `LABEL_AI MODIFIED_black transparent.png` | `f48f6c5137834fff87c701eb9a44934ac51c6b5d826b9b5cfead1b88024f8387` |
|
||||
| `LABEL_AI MODIFIED_black.png` | `0f32e4afbe1459baaf425c78373ea541421ecdb61803438892e2fc07df67da8f` |
|
||||
| `LABEL_AI MODIFIED_white transparent.png` | `1f3b435b0bf775a168309c4ede49b23c7fa94b759a72623932e6ccb7bdef6203` |
|
||||
| `LABEL_AI MODIFIED_white.png` | `64241cf4f87eb0837bd234cb63716c7d78f4e9dbbb8866f40464ca4e6bfbfead` |
|
||||
| `LABEL_AI_black transparent.png` | `24da93847c04a5c86c714c6cd018c45331b47df32936b617b3c8476933d97fa9` |
|
||||
| `LABEL_AI_black.png` | `0814d5f04533c118f81aed0d353ea75d1df4171c40b962dba1dfa044a3d84779` |
|
||||
| `LABEL_AI_white transparent.png` | `5b8c743b679a62453a4bc25070b82bb4db33ad9c68f4ef1b6ef7bb562dd3a4e3` |
|
||||
| `LABEL_AI_white.png` | `b9041e416fed455722a3aaa06a5ea0eb376d8a8e11385fdc8e3e8ee6998cdf26` |
|
||||
|
||||
A policy whose `visual` is one of these digests shows that EU icon; any other digest names an
|
||||
equivalent label, which the Code allows (Section 2, Commitment 1). A player that shows a pinned
|
||||
icon shows exactly those bytes. No audible EU label is published: the Annex says an audio-only
|
||||
icon will be developed, and the Code's audio work begins with a task force. A policy's `audio` is
|
||||
then the digest of the disclaimer or earcon its signer chooses (Sub-measure 1.2.3), until a
|
||||
revision of this table pins a published one.
|
||||
|
||||
### 10.3 The player's log
|
||||
|
||||
A player signs a **log** of a viewing session, one record per entry, `kind` `"wai-player"`, under a
|
||||
key of that session (no identity of the viewer or the device):
|
||||
|
||||
| member | type | meaning |
|
||||
|---|---|---|
|
||||
| `kind` | string | `"wai-player"` |
|
||||
| `session` | 32 B hex | fresh random bytes of the session, the same in every entry |
|
||||
| `seq` | integer | the entry's position, from 0 |
|
||||
| `prev` | 32 B hex or `null` | the link hash of the entry before it; `null` for entry 0 |
|
||||
| `at` | integer | the session clock at the entry, in microseconds; never below the entry before it's |
|
||||
| `entry` | object | one of the four below |
|
||||
| `signer_pubkey`, `sig` | | §2, the same key in every entry |
|
||||
|
||||
The entries:
|
||||
|
||||
- `{"type": "present", "subject", "origin", "class", "media", "tier", "decoder", "variant",
|
||||
"output"}`: the player presented an object from `at` on: its content hash; the link hash of the
|
||||
origin claim it applied, or `null`; that claim's class as it read it, or `null`; the `media` of
|
||||
§3.1; the tier its path holds (`decode-equivalence`, `generation-equivalence`,
|
||||
`entropy-consistency` or `none`, §9.2); its decoder (a term) and variant (a term or `null`), as a
|
||||
`reconstruct-session` claim names them (jwp-receipts §2.4.12); and its output's digest, or `null`
|
||||
where it states none.
|
||||
- `{"type": "shown", "label", "asset", "trigger"}`: the player showed a label: `visual` or `audio`;
|
||||
the SHA-256 of the asset shown; and why, as the player states it: `first-exposure`, `resume` or
|
||||
`interval`.
|
||||
- `{"type": "resume", "after"}`: presentation resumed after an interruption, `ad-break` or
|
||||
`other`.
|
||||
- `{"type": "review", "subject", "review"}`: the player presented the review whose link hash is
|
||||
`review` for the object `subject` (§9.2).
|
||||
|
||||
A log whose `seq` is not each entry's position, whose `prev` is not the link hash of the entry
|
||||
before, whose `at` goes back, or whose `session` or key changes, is `player_chain_broken`, at the
|
||||
first entry that breaks it.
|
||||
|
||||
### 10.4 The obligations
|
||||
|
||||
Each `present` entry whose `origin` names a claim the verifier holds states that claim's class,
|
||||
for that claim's object (`present_mismatch`). Under a label policy, in this order:
|
||||
|
||||
1. Every `shown` entry's asset is the policy's `visual` for a visual label and its `audio` for an
|
||||
audible one (`label_asset_mismatch`).
|
||||
2. Each `present` entry whose `class` is one the policy labels is a **labelled presentation** at
|
||||
its `at`, *p*, of the label kind its `media` asks for (§10.1). Taking them in the log's order,
|
||||
each kind on its own:
|
||||
- the first of its kind is a **first exposure**: a `shown` entry of that kind is in the log
|
||||
with `at` at most *p* + `grace_us`;
|
||||
- a later one, when a `resume` entry lies between the labelled presentation of its kind
|
||||
before it and it, is a **resume**: a `shown` entry of that kind has `at` from the latest such
|
||||
`resume`'s `at` to *p* + `grace_us`;
|
||||
- any other, under a policy with an interval, is an **interval**: a `shown` entry of that kind
|
||||
has `at` from *p* − `interval_us` to *p* + `grace_us`.
|
||||
|
||||
One with no such `shown` entry is `label_not_shown`, at its `present` entry. A label not shown
|
||||
at first exposure is caught by the player's own signed log: the log states the presentation and
|
||||
when the label came.
|
||||
|
||||
## 11. Verification
|
||||
|
||||
### 11.1 Inputs
|
||||
|
||||
A verifier is given, each in a list: origin claims, marking records, detection records and
|
||||
reviews; a label policy or none; a player's log or none; the envelopes of the objects it holds
|
||||
(each named by its content hash); the decoded samples it holds, each with the content hash of
|
||||
the object decoded; a room or none, as live-rooms §9.1 gives a room verifier its inputs; and a
|
||||
**policy** of two members:
|
||||
|
||||
| member | values | default | meaning |
|
||||
|---|---|---|---|
|
||||
| `dual_layer` | `when-given`, `required` | `when-given` | `required`: §7.1's rule applies (`layer_missing`) |
|
||||
| `assistive` | `accept-asserted`, `require-proof` | `accept-asserted` | `require-proof`: §4.4 step 4 applies (`assistive_not_proved`) |
|
||||
|
||||
### 11.2 Steps
|
||||
|
||||
The verifier runs these steps and stops at the first refusal, which it reports with the reason
|
||||
and where: a record by its list and index, the label policy, or the room as live-rooms §9.1 states
|
||||
a place in it.
|
||||
|
||||
1. **Reading.** Each record, list by list (origin claims, markings, detections, reviews, the label
|
||||
policy, the log's entries), each in order, reads (§2, §3.1, §3.2, §4.3, §7.2, §7.3, §9.1, §10.1,
|
||||
§10.3) and its signature verifies: `record_malformed`, `bad_key`, `bad_signature`.
|
||||
2. **Origin claims**, in order, each by these checks in order: a subject named before
|
||||
(`claim_conflict`); its class (§3.2, `class_mismatch`); its source (§5, `source_mismatch`); its
|
||||
inputs and its envelope (§3.3: `input_mismatch`, `class_understated`); its evidence (§4.4:
|
||||
`evidence_invalid`, `input_samples_mismatch`, `assistive_recompute_mismatch`,
|
||||
`samples_mismatch`, `assistive_not_proved`).
|
||||
3. **Marking.** The markings in order (§7.2: `claim_unknown`, `marking_mismatch`); the detections
|
||||
in order (§7.3: `detection_mismatch`, `watermark_absent`, `layers_disagree`); then, under
|
||||
`dual_layer` `required`, the origin claims in order (§7.1, `layer_missing`).
|
||||
4. **Reviews**, in order (§9.1: `claim_unknown`, `review_subject_mismatch`).
|
||||
5. **The room.** Live-rooms §9: a refusal there is this verification's, at the room. A gap is not
|
||||
a refusal. Then its AI participants' tracks (§8.2, `ai_track_unlabelled`).
|
||||
6. **The log.** Its chain (§10.3, `player_chain_broken`); then each entry in order: a `present`
|
||||
entry (§10.4, `present_mismatch`; §9.2, `present_output_irreproducible`) and a `review` entry
|
||||
(§9.2: `claim_unknown`, `review_subject_mismatch`, `review_rejected`, `review_path_not_exact`,
|
||||
`review_reconstruction_mismatch`); then, under a label policy, §10.4's steps 1 and 2
|
||||
(`label_asset_mismatch`, `label_not_shown`).
|
||||
|
||||
### 11.3 What an accepted case states
|
||||
|
||||
- for each origin claim, in order: its link hash, class and source, its C2PA statement (§6) as
|
||||
written, and for an assist claim whether it is `proved`, `not-recomputed` or `asserted` (§4.4);
|
||||
- for each detection, in order: its object and its result, `agree`, `unmarked` or `not-checked`
|
||||
(§7.3);
|
||||
- for each AI participant's track, in the order §8.2 reaches them: its digest and the claim that
|
||||
labels it;
|
||||
- for each `review` entry of the log: its index and the review's link hash;
|
||||
- for each labelled presentation that had a label due: its index, the trigger (`first-exposure`,
|
||||
`resume` or `interval`) and the index of the first `shown` entry that met it;
|
||||
- the file name of the EU icon the label policy's `visual` is, where it is one (§10.2);
|
||||
- the room's own outcome, `accept` or `gap`, where a room was given.
|
||||
|
||||
## 12. Reasons
|
||||
|
||||
The codes this extension adds, each with where it is raised:
|
||||
|
||||
| reason | raised at |
|
||||
|---|---|
|
||||
| `record_malformed` | §2, §3.1, §3.2, §4.3, §11.2 step 1 |
|
||||
| `bad_key` | §2 |
|
||||
| `bad_signature` | §2 |
|
||||
| `claim_conflict` | §3.1 |
|
||||
| `class_mismatch` | §3.2 |
|
||||
| `source_mismatch` | §5 |
|
||||
| `input_mismatch` | §3.3 |
|
||||
| `class_understated` | §3.3 |
|
||||
| `evidence_invalid` | §4.3, §4.4 step 1 |
|
||||
| `input_samples_mismatch` | §4.4 step 2 |
|
||||
| `assistive_recompute_mismatch` | §4.4 step 2 |
|
||||
| `samples_mismatch` | §4.4 step 3 |
|
||||
| `assistive_not_proved` | §4.4 step 4 |
|
||||
| `claim_unknown` | §7.2, §9.1, §9.2 |
|
||||
| `marking_mismatch` | §7.2 |
|
||||
| `detection_mismatch` | §7.3 |
|
||||
| `watermark_absent` | §7.3 |
|
||||
| `layers_disagree` | §7.3 |
|
||||
| `layer_missing` | §7.1 |
|
||||
| `ai_track_unlabelled` | §8.2 |
|
||||
| `player_chain_broken` | §10.3 |
|
||||
| `present_mismatch` | §10.4 |
|
||||
| `present_output_irreproducible` | §9.2 |
|
||||
| `review_subject_mismatch` | §9.1, §9.2 |
|
||||
| `review_rejected` | §9.2 |
|
||||
| `review_path_not_exact` | §9.2 |
|
||||
| `review_reconstruction_mismatch` | §9.2 |
|
||||
| `label_asset_mismatch` | §10.4 |
|
||||
| `label_not_shown` | §10.4 |
|
||||
|
||||
A verifier given a room also raises every code a room verifier raises (live-rooms §11, and the
|
||||
live-events codes it lists), at the room: `ai_unmarked` among them, for an AI participant's
|
||||
event that does not state its machine origin (live-rooms §9.2 step 6).
|
||||
|
||||
## 13. Conformance
|
||||
|
||||
[`live-conformance/synthesis/`](../live-conformance/synthesis/) holds the corpus, in one file,
|
||||
`vectors.json`, written from fixed seeds by `wai_synthesis_vectors gen`. It lists the EU icons of
|
||||
§10.2 and the **vectors**: each a case of §11.1, its outcome, and, accepted, everything §11.3
|
||||
states; refused, its reason and its place. It has:
|
||||
|
||||
- **Accepted**: a capture, a neural codec's reconstruction of it, and its two pinned derivations,
|
||||
each proved by recomputation from the reconstruction's decoded samples; a float
|
||||
super-resolution step whose attest evidence is asserted; a generated video, an algorithmic
|
||||
audio generation from a `wai.generate.spectral` envelope, an inpainting a person reviewed, a
|
||||
composite of a capture and the generated video, and a transcode of the video, each synthesized
|
||||
object marked in both layers, a detection that agrees and one that finds the capture unmarked;
|
||||
a room whose AI host states its model in its chat and caption and labels its speech track; and a
|
||||
review of the generated video presented on an exact path, with labels shown at first exposure,
|
||||
after an ad break and within an interval.
|
||||
- **Refused**: each reason of §12 at least once, each a control of one defect: among them a
|
||||
synthesized object labelled captured (`class_understated`), an assistive claim whose
|
||||
recomputation differs (`assistive_recompute_mismatch`), the two marking layers disagreeing
|
||||
(`layers_disagree`), a review presented on a non-exact path (`review_path_not_exact`), an AI
|
||||
participant's unmarked chat (`ai_unmarked`, raised by the room) and a track it publishes with no
|
||||
claim (`ai_track_unlabelled`), and a label not shown at first exposure, after a break, and within
|
||||
the interval (`label_not_shown`).
|
||||
|
||||
A conforming verifier gives every vector the outcome, the reason, the place and the statements
|
||||
the corpus states. `wai_synthesis_vectors verify` and the test `tests/synthesis.rs` check that the
|
||||
file regenerates byte for byte, that `wai-rs` gives every vector its outcome, that §12 lists
|
||||
exactly the reference's codes, and that §10.2's table pins exactly the reference's icons.
|
||||
`live-conformance/synthesis_verify.py` is the second implementation, written from this text: it
|
||||
shares no code with `wai-rs`; it reads §10.2's pins and §12's codes from this text,
|
||||
verifies each room with the live-rooms corpus's second implementation (`live_rooms_verify.py`),
|
||||
every signature with WAI's strict rule (`claim-conformance/_ed25519.py`) and every link hash with
|
||||
BLAKE3 written from its specification (`claim-conformance/_blake3.py`); and it mutates copies of
|
||||
the corpus and requires every copy to be refused.
|
||||
|
||||
## 14. Security and privacy considerations
|
||||
|
||||
- A claim carries its signer's authority. A class is a statement; what this extension adds is
|
||||
that a class can only rise along a chain of claims (§3.2), that an assistive claim over a pinned
|
||||
derivation is checked by recomputation (§4), that a generator's own envelope cannot be claimed
|
||||
as anything but generated (§3.3), and that the second layer is checked against the first (§7).
|
||||
A synthesized object whose producer signs no claim at all is caught only by the second layer,
|
||||
and only once a watermark scheme is registered (§7.4).
|
||||
- A watermark payload's seven bytes of link hash locate a claim; they are not a commitment, and a
|
||||
mark can be copied into other content. A detection that agrees with a claim says the content
|
||||
carries that claim's mark, not that the content is the claim's subject; the claim's subject is
|
||||
bound by content hash.
|
||||
- Evidence states digests of samples, never samples. A verifier that recomputes holds the input's
|
||||
decoded samples, which it decoded itself.
|
||||
- A player's log is signed under a key of the session and names objects, claims, labels and
|
||||
instants of the session's clock: no viewer, no device and no account. It shows what that key
|
||||
reports, and is not evidence that a person watched.
|
||||
- A review names its reviewer's key and, where given, a role the reviewer states; a role is the
|
||||
reviewer's statement until an authority attests it.
|
||||
- A relay can withhold a claim but cannot alter one. A sink that receives an object without the
|
||||
claim its chain should hold sees the gap in the chain of inputs (§3.4).
|
||||
|
|
@ -18,6 +18,7 @@ and the independent verifier of each, beside it.
|
|||
| [`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` |
|
||||
| [`c2pa-live/`](c2pa-live/) | [live-provenance](../extensions/live-provenance.md) (`wai.live.provenance`) | `c2pa_live_verify.py` |
|
||||
| [`synthesis/`](synthesis/) | [synthesis](../extensions/synthesis.md) (`wai.media.synthesis`) | `synthesis_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
|
||||
|
|
@ -26,12 +27,12 @@ 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/`, `wai_live_ads_vectors gen` writes
|
||||
`ads/` and `wai_live_provenance_vectors gen` writes `c2pa-live/`, each from records authored
|
||||
in code, from fixed seeds (`events/README.md`,
|
||||
`ads/`, `wai_live_provenance_vectors gen` writes `c2pa-live/` and `wai_synthesis_vectors gen` writes
|
||||
`synthesis/`, 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`, `c2pa-live/README.md`); `c2pa-live/` cuts its media
|
||||
from the packaging corpus's AV1 floor. `joule_moq_e2ee_vectors gen` (in
|
||||
`watermark/README.md`, `ads/README.md`, `c2pa-live/README.md`, `synthesis/README.md`);
|
||||
`c2pa-live/` cuts its media from the packaging corpus's AV1 floor. `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
|
||||
|
|
@ -55,4 +56,5 @@ python3 live_ads_verify.py ads
|
|||
python3 e2ee_verify.py e2ee
|
||||
python3 live_watermark_verify.py watermark
|
||||
python3 c2pa_live_verify.py c2pa-live
|
||||
python3 synthesis_verify.py synthesis
|
||||
```
|
||||
|
|
|
|||
|
|
@ -432,6 +432,52 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "kind_chat_origin",
|
||||
"about": "chat and reaction stating their origin (§4.4, §4.5): a machine message with its model's digest, a machine-reviewed one, a human one stating origin 1, and a machine reaction targeting the first",
|
||||
"room": "main",
|
||||
"default_policy": "default",
|
||||
"policies": {},
|
||||
"events": [
|
||||
"5745563101994ce44cd8c3fa2781c7976335330a5e718019657440cb46a77db3210900ce31c2a1e8c1326f0811f77d5ef3628991d76af48f4a089e7ed08541846e351fce930000000000000000000000000000000000000000000000000000000000000000000000000000000040420f00000000000003480025004865726520697320612073756d6d617279206f66207468652074616c6b20736f206661722e02b16dcccea3b663fa41a0b4c59228cb2dddbbf2510df387ed330dc4a11aeeae1cd1c8363bda75e23afcaba414d396c3dcdeaf82c1f2954261ccb7d20aa5a527d65cf9a59fb50c9992e1afe2b51a87df6093dc255bddd3ca137bdef58caecf160c",
|
||||
"5745563101994ce44cd8c3fa2781c7976335330a5e718019657440cb46a77db3210900ce31c2a1e8c1326f0811f77d5ef3628991d76af48f4a089e7ed08541846e351fce9301000000000000002ff65886fa1f94c8cd7c18a84d052b6f8e1d32c36f975bbc8f23048fef770f3580841e000000000000034b00280052657669657765643a207468652073756d6d6172792061626f76652069732061636375726174652e03b16dcccea3b663fa41a0b4c59228cb2dddbbf2510df387ed330dc4a11aeeae1c74553836741ceae7553cc599849aa68d35290a1e831a027c82a9a6def82b8f28dd6024eed3368c80a22f37559328e881733901207ba09ea7a2a9a1f55acb5607",
|
||||
"5745563101994ce44cd8c3fa2781c7976335330a5e718019657440cb46a77db3210900ce31c2a1e8c1326f0811f77d5ef3628991d76af48f4a089e7ed08541846e351fce9302000000000000007bcf54fed4a5769d4c7987599718e6f2359e9f6fb17cf2eb754b6b79f034c116c0c62d00000000000003150012005479706564206279206120706572736f6e2e012df40b7fb2b654619a6d2b19133102bcbdb6b7bb5bc936a9bde2a6924842041a561080468342519243994d6e34b466c8b380ac4da5a5454739460eb48e6a8d06",
|
||||
"5745563101994ce44cd8c3fa2781c7976335330a5e718019657440cb46a77db3210900ce31c2a1e8c1326f0811f77d5ef3628991d76af48f4a089e7ed08541846e351fce930300000000000000a806a8ec963a5a2bea734fd5fbdaeb68eb8890a7b1ac0e67ed8106decbe90bf800093d000000000000044500010100012ff65886fa1f94c8cd7c18a84d052b6f8e1d32c36f975bbc8f23048fef770f3502b16dcccea3b663fa41a0b4c59228cb2dddbbf2510df387ed330dc4a11aeeae1c06d90f8c5c3615fed522ab121c32ad98b740c874c0ebd1b391f55162d1a0ecec81fa90563807f382142d37ee1868d4107291c9456fa8bdd8630d0ca8f9b69f07"
|
||||
],
|
||||
"outcome": "accept",
|
||||
"digests": [
|
||||
"2ff65886fa1f94c8cd7c18a84d052b6f8e1d32c36f975bbc8f23048fef770f35",
|
||||
"7bcf54fed4a5769d4c7987599718e6f2359e9f6fb17cf2eb754b6b79f034c116",
|
||||
"a806a8ec963a5a2bea734fd5fbdaeb68eb8890a7b1ac0e67ed8106decbe90bf8",
|
||||
"e262cac60f3b23c92d7e3f64aa7af8725c7f8199f5fc3588051b46a32e997a9c"
|
||||
],
|
||||
"tally": {
|
||||
"state_hash": "771b30268d4b5a93046d9e87dbbc10bae6fc76c4c2d5dde9071f801bfdb9a68d",
|
||||
"polls": []
|
||||
},
|
||||
"bindings": [
|
||||
{
|
||||
"msf": "{\"t\":1792108801000,\"data\":{\"wev1\":\"V0VWMQGZTORM2MP6J4HHl2M1MwpecYAZZXRAy0anfbMhCQDOMcKh6MEybwgR931e82KJkddq9I9KCJ5-0IVBhG41H86TAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBCDwAAAAAAAANIACUASGVyZSBpcyBhIHN1bW1hcnkgb2YgdGhlIHRhbGsgc28gZmFyLgKxbczOo7Zj-kGgtMWSKMst3bvyUQ3zh-0zDcShGu6uHNHINjvadeI6_KukFNOWw9zer4LB8pVCYcy30gqlpSfWXPmln7UMmZLhr-K1GoffYJPcJVvd08oTe971jK7PFgw\"}}",
|
||||
"daterange": "#EXT-X-DATERANGE:ID=\"2ff65886fa1f94c8cd7c18a84d052b6f8e1d32c36f975bbc8f23048fef770f35\",CLASS=\"science.transaction.wai.event\",START-DATE=\"2026-10-16T00:00:01.000Z\",X-WAI-EVENT=\"V0VWMQGZTORM2MP6J4HHl2M1MwpecYAZZXRAy0anfbMhCQDOMcKh6MEybwgR931e82KJkddq9I9KCJ5-0IVBhG41H86TAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBCDwAAAAAAAANIACUASGVyZSBpcyBhIHN1bW1hcnkgb2YgdGhlIHRhbGsgc28gZmFyLgKxbczOo7Zj-kGgtMWSKMst3bvyUQ3zh-0zDcShGu6uHNHINjvadeI6_KukFNOWw9zer4LB8pVCYcy30gqlpSfWXPmln7UMmZLhr-K1GoffYJPcJVvd08oTe971jK7PFgw\"",
|
||||
"emsg": "00000136656d736701000000000003e80000000000002af8000000002ff6588675726e3a7761693a6576656e743a310063686174005745563101994ce44cd8c3fa2781c7976335330a5e718019657440cb46a77db3210900ce31c2a1e8c1326f0811f77d5ef3628991d76af48f4a089e7ed08541846e351fce930000000000000000000000000000000000000000000000000000000000000000000000000000000040420f00000000000003480025004865726520697320612073756d6d617279206f66207468652074616c6b20736f206661722e02b16dcccea3b663fa41a0b4c59228cb2dddbbf2510df387ed330dc4a11aeeae1cd1c8363bda75e23afcaba414d396c3dcdeaf82c1f2954261ccb7d20aa5a527d65cf9a59fb50c9992e1afe2b51a87df6093dc255bddd3ca137bdef58caecf160c"
|
||||
},
|
||||
{
|
||||
"msf": "{\"t\":1792108802000,\"data\":{\"wev1\":\"V0VWMQGZTORM2MP6J4HHl2M1MwpecYAZZXRAy0anfbMhCQDOMcKh6MEybwgR931e82KJkddq9I9KCJ5-0IVBhG41H86TAQAAAAAAAAAv9liG-h-UyM18GKhNBStvjh0yw2-XW7yPIwSP73cPNYCEHgAAAAAAAANLACgAUmV2aWV3ZWQ6IHRoZSBzdW1tYXJ5IGFib3ZlIGlzIGFjY3VyYXRlLgOxbczOo7Zj-kGgtMWSKMst3bvyUQ3zh-0zDcShGu6uHHRVODZ0HOrnVTzFmYSapo01KQoegxoCfIKppt74K48o3WAk7tM2jICiLzdVkyjogXM5ASB7oJ6noqmh9VrLVgc\"}}",
|
||||
"daterange": "#EXT-X-DATERANGE:ID=\"7bcf54fed4a5769d4c7987599718e6f2359e9f6fb17cf2eb754b6b79f034c116\",CLASS=\"science.transaction.wai.event\",START-DATE=\"2026-10-16T00:00:02.000Z\",X-WAI-EVENT=\"V0VWMQGZTORM2MP6J4HHl2M1MwpecYAZZXRAy0anfbMhCQDOMcKh6MEybwgR931e82KJkddq9I9KCJ5-0IVBhG41H86TAQAAAAAAAAAv9liG-h-UyM18GKhNBStvjh0yw2-XW7yPIwSP73cPNYCEHgAAAAAAAANLACgAUmV2aWV3ZWQ6IHRoZSBzdW1tYXJ5IGFib3ZlIGlzIGFjY3VyYXRlLgOxbczOo7Zj-kGgtMWSKMst3bvyUQ3zh-0zDcShGu6uHHRVODZ0HOrnVTzFmYSapo01KQoegxoCfIKppt74K48o3WAk7tM2jICiLzdVkyjogXM5ASB7oJ6noqmh9VrLVgc\"",
|
||||
"emsg": "00000139656d736701000000000003e80000000000002ee0000000007bcf54fe75726e3a7761693a6576656e743a310063686174005745563101994ce44cd8c3fa2781c7976335330a5e718019657440cb46a77db3210900ce31c2a1e8c1326f0811f77d5ef3628991d76af48f4a089e7ed08541846e351fce9301000000000000002ff65886fa1f94c8cd7c18a84d052b6f8e1d32c36f975bbc8f23048fef770f3580841e000000000000034b00280052657669657765643a207468652073756d6d6172792061626f76652069732061636375726174652e03b16dcccea3b663fa41a0b4c59228cb2dddbbf2510df387ed330dc4a11aeeae1c74553836741ceae7553cc599849aa68d35290a1e831a027c82a9a6def82b8f28dd6024eed3368c80a22f37559328e881733901207ba09ea7a2a9a1f55acb5607"
|
||||
},
|
||||
{
|
||||
"msf": "{\"t\":1792108803000,\"data\":{\"wev1\":\"V0VWMQGZTORM2MP6J4HHl2M1MwpecYAZZXRAy0anfbMhCQDOMcKh6MEybwgR931e82KJkddq9I9KCJ5-0IVBhG41H86TAgAAAAAAAAB7z1T-1KV2nUx5h1mXGObyNZ6fb7F88ut1S2t58DTBFsDGLQAAAAAAAAMVABIAVHlwZWQgYnkgYSBwZXJzb24uAS30C3-ytlRhmm0rGRMxAry9tre7W8k2qb3ippJIQgQaVhCARoNCUZJDmU1uNLRmyLOArE2lpUVHOUYOtI5qjQY\"}}",
|
||||
"daterange": "#EXT-X-DATERANGE:ID=\"a806a8ec963a5a2bea734fd5fbdaeb68eb8890a7b1ac0e67ed8106decbe90bf8\",CLASS=\"science.transaction.wai.event\",START-DATE=\"2026-10-16T00:00:03.000Z\",X-WAI-EVENT=\"V0VWMQGZTORM2MP6J4HHl2M1MwpecYAZZXRAy0anfbMhCQDOMcKh6MEybwgR931e82KJkddq9I9KCJ5-0IVBhG41H86TAgAAAAAAAAB7z1T-1KV2nUx5h1mXGObyNZ6fb7F88ut1S2t58DTBFsDGLQAAAAAAAAMVABIAVHlwZWQgYnkgYSBwZXJzb24uAS30C3-ytlRhmm0rGRMxAry9tre7W8k2qb3ippJIQgQaVhCARoNCUZJDmU1uNLRmyLOArE2lpUVHOUYOtI5qjQY\"",
|
||||
"emsg": "00000103656d736701000000000003e800000000000032c800000000a806a8ec75726e3a7761693a6576656e743a310063686174005745563101994ce44cd8c3fa2781c7976335330a5e718019657440cb46a77db3210900ce31c2a1e8c1326f0811f77d5ef3628991d76af48f4a089e7ed08541846e351fce9302000000000000007bcf54fed4a5769d4c7987599718e6f2359e9f6fb17cf2eb754b6b79f034c116c0c62d00000000000003150012005479706564206279206120706572736f6e2e012df40b7fb2b654619a6d2b19133102bcbdb6b7bb5bc936a9bde2a6924842041a561080468342519243994d6e34b466c8b380ac4da5a5454739460eb48e6a8d06"
|
||||
},
|
||||
{
|
||||
"msf": "{\"t\":1792108804000,\"data\":{\"wev1\":\"V0VWMQGZTORM2MP6J4HHl2M1MwpecYAZZXRAy0anfbMhCQDOMcKh6MEybwgR931e82KJkddq9I9KCJ5-0IVBhG41H86TAwAAAAAAAACoBqjsljpaK-pzT9X72uto64iQp7GsDmftgQbey-kL-AAJPQAAAAAAAARFAAEBAAEv9liG-h-UyM18GKhNBStvjh0yw2-XW7yPIwSP73cPNQKxbczOo7Zj-kGgtMWSKMst3bvyUQ3zh-0zDcShGu6uHAbZD4xcNhX-1SKrEhwyrZi3QMh0wOvRs5H1UWLRoOzsgfqQVjgH84IULTfuGGjUEHKRyUVvqL3YYw0MqPm2nwc\"}}",
|
||||
"daterange": "#EXT-X-DATERANGE:ID=\"e262cac60f3b23c92d7e3f64aa7af8725c7f8199f5fc3588051b46a32e997a9c\",CLASS=\"science.transaction.wai.event\",START-DATE=\"2026-10-16T00:00:04.000Z\",X-WAI-EVENT=\"V0VWMQGZTORM2MP6J4HHl2M1MwpecYAZZXRAy0anfbMhCQDOMcKh6MEybwgR931e82KJkddq9I9KCJ5-0IVBhG41H86TAwAAAAAAAACoBqjsljpaK-pzT9X72uto64iQp7GsDmftgQbey-kL-AAJPQAAAAAAAARFAAEBAAEv9liG-h-UyM18GKhNBStvjh0yw2-XW7yPIwSP73cPNQKxbczOo7Zj-kGgtMWSKMst3bvyUQ3zh-0zDcShGu6uHAbZD4xcNhX-1SKrEhwyrZi3QMh0wOvRs5H1UWLRoOzsgfqQVjgH84IULTfuGGjUEHKRyUVvqL3YYw0MqPm2nwc\"",
|
||||
"emsg": "00000137656d736701000000000003e800000000000036b000000000e262cac675726e3a7761693a6576656e743a31007265616374696f6e005745563101994ce44cd8c3fa2781c7976335330a5e718019657440cb46a77db3210900ce31c2a1e8c1326f0811f77d5ef3628991d76af48f4a089e7ed08541846e351fce930300000000000000a806a8ec963a5a2bea734fd5fbdaeb68eb8890a7b1ac0e67ed8106decbe90bf800093d000000000000044500010100012ff65886fa1f94c8cd7c18a84d052b6f8e1d32c36f975bbc8f23048fef770f3502b16dcccea3b663fa41a0b4c59228cb2dddbbf2510df387ed330dc4a11aeeae1c06d90f8c5c3615fed522ab121c32ad98b740c874c0ebd1b391f55162d1a0ecec81fa90563807f382142d37ee1868d4107291c9456fa8bdd8630d0ca8f9b69f07"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "kind_caption",
|
||||
"about": "caption (§4.10): human, machine and machine-reviewed origins, the latter two with a model digest, three language tags, and a caption with no stated duration",
|
||||
|
|
@ -1420,6 +1466,19 @@
|
|||
"reason": "malformed_payload",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"name": "malformed_payload_origin",
|
||||
"about": "a chat stating origin 2 with no model digest after it (§4.4)",
|
||||
"room": "main",
|
||||
"default_policy": "default",
|
||||
"policies": {},
|
||||
"events": [
|
||||
"5745563101994ce44cd8c3fa2781c7976335330a5e718019657440cb46a77db3210900ce31c2a1e8c1326f0811f77d5ef3628991d76af48f4a089e7ed08541846e351fce930000000000000000000000000000000000000000000000000000000000000000000000000000000040420f000000000000030f000c007768696368206d6f64656c3f02b8bbfca6c533438f2f05fdd56a1354fe50609951f58fef3cd0087f8bb25afe82d87dc1114046480dc6a6a18df663a91b25172f1c6c356084519d478b6107f207"
|
||||
],
|
||||
"outcome": "refuse",
|
||||
"reason": "malformed_payload",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"name": "malformed_payload_poll",
|
||||
"about": "a poll_open with one option (§4.6)",
|
||||
|
|
|
|||
|
|
@ -164,6 +164,19 @@ def splice_info_section_ok(b):
|
|||
return len(b) >= 3 and b[0] == 0xFC and 3 + (((b[1] & 0x0F) << 8) | b[2]) == len(b)
|
||||
|
||||
|
||||
def stated_origin(r):
|
||||
"""§4.4, §4.5: the origin a chat or a reaction may state after its last field, as
|
||||
(origin, model), or (None, None) when no byte follows."""
|
||||
if r.done():
|
||||
return None, None
|
||||
o = r.u8()
|
||||
if o == 1:
|
||||
return 1, None
|
||||
if o in (2, 3):
|
||||
return o, r.take(32)
|
||||
raise Refuse("malformed_payload")
|
||||
|
||||
|
||||
def read_payload(kind, b, max_cp):
|
||||
"""§4: the payload as a dict, or Refuse."""
|
||||
m = "malformed_payload"
|
||||
|
|
@ -188,6 +201,7 @@ def read_payload(kind, b, max_cp):
|
|||
raise Refuse(m)
|
||||
elif kind == 0x03:
|
||||
p["text"] = text(r, max_cp)
|
||||
p["origin"], p["model"] = stated_origin(r)
|
||||
elif kind == 0x04:
|
||||
p["type"] = r.u8()
|
||||
if p["type"] not in (1, 2, 3, 4, 5):
|
||||
|
|
@ -200,6 +214,7 @@ def read_payload(kind, b, max_cp):
|
|||
raise Refuse(m)
|
||||
p["target"] = r.take(32) if flags & 1 else None
|
||||
p["remove"] = bool(flags & 2)
|
||||
p["origin"], p["model"] = stated_origin(r)
|
||||
elif kind == 0x05:
|
||||
p["question"] = text(r, max_cp)
|
||||
n = r.u8()
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ REASONS = [
|
|||
"grant_exceeds_room", "exchange_unlinked", "exchange_mismatch", "lifecycle_malformed",
|
||||
"grant_unknown", "grant_wrong_principal", "grant_revoked", "grant_reused",
|
||||
"already_present", "not_present", "participant_limit", "outside_grant_window",
|
||||
"power_not_granted", "publish_not_permitted", "profile_not_granted", "already_published",
|
||||
"power_not_granted", "ai_unmarked", "publish_not_permitted", "profile_not_granted", "already_published",
|
||||
"not_published", "slot_unknown", "slot_full", "publisher_limit", "exchange_not_current",
|
||||
"exchange_off_tick", "takeover_not_higher", "failover_not_standby", "failover_early",
|
||||
"pop_missing", "pop_malformed", "pop_wrong_grant", "pop_wrong_key", "pop_bad_signature",
|
||||
|
|
@ -395,11 +395,22 @@ def grant_fields_ok(g, d, held):
|
|||
media.add(m)
|
||||
if len(g["origins"]) > 8 or not all(is_token(o, 255) for o in g["origins"]):
|
||||
return False
|
||||
if read_attrs(g["attrs"]) is None:
|
||||
attrs = read_attrs(g["attrs"])
|
||||
if attrs is None:
|
||||
return False
|
||||
if any(k == "ai" and not re.fullmatch(r"[0-9a-f]{64}", v) for k, v in attrs):
|
||||
return False
|
||||
return all(h["digest"] != g["digest"] for h in held)
|
||||
|
||||
|
||||
def ai_model(g):
|
||||
"""§4.4: the model an AI participant's grant names, as bytes, or None."""
|
||||
for k, v in read_attrs(g["attrs"]) or []:
|
||||
if k == "ai":
|
||||
return bytes.fromhex(v)
|
||||
return None
|
||||
|
||||
|
||||
def publishes(g, m):
|
||||
return m in MEDIA and g["publish"] >> (m - 1) & 1 == 1
|
||||
|
||||
|
|
@ -1035,6 +1046,10 @@ def step(d, room, ver, seen, auth, b):
|
|||
need = RECORD if payload["command"] in (1, 2) else DIRECT
|
||||
if g["powers"] & need == 0:
|
||||
raise Refuse("power_not_granted")
|
||||
model = ai_model(g)
|
||||
if model is not None and payload["kind"] in (0x03, 0x04, 0x0A):
|
||||
if payload["origin"] not in (2, 3) or payload["model"] != model:
|
||||
raise Refuse("ai_unmarked")
|
||||
return g["digest"]
|
||||
|
||||
|
||||
|
|
@ -1124,7 +1139,7 @@ def main(corpus_dir):
|
|||
missing = [r for r in REASONS if r not in reasons_seen]
|
||||
if missing:
|
||||
fails.append("no vector refused with %s" % ", ".join(missing))
|
||||
rooms = ["lesson", "broadcast", "takeover", "redundant_failover", "promote_viewer", "dvr"]
|
||||
rooms = ["lesson", "broadcast", "takeover", "redundant_failover", "promote_viewer", "dvr", "ai_participant"]
|
||||
absent = [r for r in rooms if outcomes.get(r) != "accept"]
|
||||
if absent:
|
||||
fails.append("rooms not accepted: %s" % ", ".join(absent))
|
||||
|
|
|
|||
|
|
@ -3529,6 +3529,23 @@
|
|||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "grant_ai_malformed",
|
||||
"about": "a grant whose ai attribute is not 64 lowercase hexadecimal digits (§4.4)",
|
||||
"descriptor": "5752443101988ec20b60d2d23c4c475c1f4e623f115ffbe7659bd0c81c0af9233c28589285c73c9d1dc51b9a83c180ec0bd17f1c8520c0ab70c4874225109cadb2d493ec1b00406cd8e95d060040420f0000000000150068747470733a2f2f726f6f6d732e6578616d706c650800000003003c0000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f00000000000504686f737407010102ae06000000000000000000000000000000000000000000000000000000000000096d6f64657261746f720001000108080000000000000000000000000000000000000000000000000000000000000861756469656e636500010100580000000000000000000000000000000000000000000000000000000000000005677565737403010100580100000000000000000000000000000000000000000000000000000000000007656e636f64657203000000000000000000000000000000000000000000000000000000000000000000000002057374616765020100000000046d61696e0202d007000002067370656563680101046f707573000001000000000000100000004000000080bb0000010f0a63616d6572612d37323002020d617630312e302e30354d2e30380b617663312e363430303146010002800268011e0096000000f40100000005d0021e00f4010000dc0500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000086c97e1a4b2609cb3438fd6c165e0bdc6f016bae7d16c4d37a387f4be3891b46f97c63fbf4814de11aadb3ee409d246a182017b43cacb6a6c64905bceb31c30b",
|
||||
"grants": [
|
||||
"575247310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a2036872df53b5dcc0c0ad294a87b904069ccadbde41494fa7e39cbf6720e1b2d5c5aa100000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a0000000000070101020000000000000000000000000000000000000000000000000000000000000000005745503101ae060000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011568747470733a2f2f72656c61792e6578616d706c65000008067f81349126761a6b7fe0cf3bd66d94800e6a65dc2f9d1d07de1a362f6d95bd8122dad96eed33117981aa9923f907d8a1a236fcf8c14579e586cbe500470b",
|
||||
"575247310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687fcb90cb92576cc567d08bedfa255bb94366c5b882a03ce247745dfd0e32ab09c00000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a000000000200010100000000000000000000000000000000000000000000000000000000000000000000574550310158000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011568747470733a2f2f72656c61792e6578616d706c6545000261694000414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141416e3520d47ed02cd4fa26c68cb2a874282c9d370a49429fd985bb64ecbca67ceaf7563a898af9d9091047cd32ad068802707bec6f1820c82b6f0ec86d65ca820b"
|
||||
],
|
||||
"key_log_head": "{\"grant_max_s\":86400,\"issued_at\":\"2026-10-15T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"room-authority\",\"key\":\"988ec20b60d2d23c4c475c1f4e623f115ffbe7659bd0c81c0af9233c28589285\",\"may_sign\":[\"live/room\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"3328656f3df5d9617e108d46080e23610712c9979d4437e706240f53d6cadf7b\",\"origin\":\"https://rooms.example\",\"prev\":null,\"revoked_modules\":[],\"root\":\"687972706312d9675734bf13b4bcf695ecb1b374de7809242aa91f130c6a1c0c\",\"seq\":0,\"sig\":\"ba55b157ebe88fc97f2b5a5e3556415563552ed88b9931060b9e99ae0a7decefdc63e2cb916b9a5d03c80a0c1823de14e7d8d3fe0b726c2464d823611780290a\",\"status_max_age_s\":3600}",
|
||||
"chains": [],
|
||||
"outcome": "refuse",
|
||||
"reason": "grant_invalid",
|
||||
"at": [
|
||||
"grant",
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "grant_attrs_too_long",
|
||||
"about": "a grant whose attributes take more than 1024 bytes (§4.4)",
|
||||
|
|
@ -4489,6 +4506,471 @@
|
|||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ai_participant",
|
||||
"about": "a host whose grant's ai attribute names a model: its chat states origin 2 and its caption origin 3, each with that model; a person's chat and reaction state none (§4.4, §9.2 step 6)",
|
||||
"descriptor": "5752443101988ec20b60d2d23c4c475c1f4e623f115ffbe7659bd0c81c0af9233c28589285c73c9d1dc51b9a83c180ec0bd17f1c8520c0ab70c4874225109cadb2d493ec1b00406cd8e95d060040420f0000000000150068747470733a2f2f726f6f6d732e6578616d706c650800000003003c0000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f00000000000504686f737407010102ae06000000000000000000000000000000000000000000000000000000000000096d6f64657261746f720001000108080000000000000000000000000000000000000000000000000000000000000861756469656e636500010100580000000000000000000000000000000000000000000000000000000000000005677565737403010100580100000000000000000000000000000000000000000000000000000000000007656e636f64657203000000000000000000000000000000000000000000000000000000000000000000000002057374616765020100000000046d61696e0202d007000002067370656563680101046f707573000001000000000000100000004000000080bb0000010f0a63616d6572612d37323002020d617630312e302e30354d2e30380b617663312e363430303146010002800268011e0096000000f40100000005d0021e00f4010000dc0500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000086c97e1a4b2609cb3438fd6c165e0bdc6f016bae7d16c4d37a387f4be3891b46f97c63fbf4814de11aadb3ee409d246a182017b43cacb6a6c64905bceb31c30b",
|
||||
"grants": [
|
||||
"575247310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a2036872df53b5dcc0c0ad294a87b904069ccadbde41494fa7e39cbf6720e1b2d5c5aa100000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a0000000000070101020000000000000000000000000000000000000000000000000000000000000000005745503101ae060000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011568747470733a2f2f72656c61792e6578616d706c6545000261694000383763386330653335666337666136613964346231393232336634303238326663333565396665633265353933383061633562336466616561653664353930385c245e4d368eea2fa46a37c069226e63a3489e51338486b574f405737169de74e0e230e5f27b523eb35cdc5e5a63fe4f104c48556ba46d8433b3db9a265eaa01",
|
||||
"575247310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687fcb90cb92576cc567d08bedfa255bb94366c5b882a03ce247745dfd0e32ab09c00000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a000000000200010100000000000000000000000000000000000000000000000000000000000000000000574550310158000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011568747470733a2f2f72656c61792e6578616d706c650000da52cb8a9a33184a5da85c37affe7d9dfc6f4e7a3019ecc07c090358ded448066ac88859718562bb805f34d7da43880addb6098e654e8e51481afb9ee22ee40e"
|
||||
],
|
||||
"key_log_head": "{\"grant_max_s\":86400,\"issued_at\":\"2026-10-15T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"room-authority\",\"key\":\"988ec20b60d2d23c4c475c1f4e623f115ffbe7659bd0c81c0af9233c28589285\",\"may_sign\":[\"live/room\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"3328656f3df5d9617e108d46080e23610712c9979d4437e706240f53d6cadf7b\",\"origin\":\"https://rooms.example\",\"prev\":null,\"revoked_modules\":[],\"root\":\"687972706312d9675734bf13b4bcf695ecb1b374de7809242aa91f130c6a1c0c\",\"seq\":0,\"sig\":\"ba55b157ebe88fc97f2b5a5e3556415563552ed88b9931060b9e99ae0a7decefdc63e2cb916b9a5d03c80a0c1823de14e7d8d3fe0b726c2464d823611780290a\",\"status_max_age_s\":3600}",
|
||||
"chains": [
|
||||
[
|
||||
"574556310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687c73c9d1dc51b9a83c180ec0bd17f1c8520c0ab70c4874225109cadb2d493ec1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001040002df53b5dcc0c0ad294a87b904069ccadbde41494fa7e39cbf6720e1b2d5c5aa13427f23d6605d4ed9be070c149aa749edd535e46fb4a813e47bdd750d546f9654022ff7403111b077255cc79fffeb37146f44c32b5f5f811ee202b6a1811fc80d874544e4e887d493655b3d5f1e26a16664ec23594e8e685b4201ff3d8c9e105",
|
||||
"574556310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687c73c9d1dc51b9a83c180ec0bd17f1c8520c0ab70c4874225109cadb2d493ec1b010000000000000066e0e16b1f2817739e3b6f2c9c87086b31f86249123017fc159142eca323af28000000000000000000104000fcb90cb92576cc567d08bedfa255bb94366c5b882a03ce247745dfd0e32ab09c37d406a857ee63ae9fecd4f293211c3e92c6847beb4871066ecfbca21f4a38607dad484d1fc66d48a45d08c1cec17d18889212fe90f204ee16684d701e5ebf4cb837bdd6dd0af53318a5465ac6f44a98f3769e7481e99169975af532a5614809"
|
||||
],
|
||||
[
|
||||
"574556310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687fcb90cb92576cc567d08bedfa255bb94366c5b882a03ce247745dfd0e32ab09c0000000000000000000000000000000000000000000000000000000000000000000000000000000040420f0000000000000312001000497320616e796f6e652074686572653f882e98ec64ee02bb24da475a9dc9c491bb077b9fbd4cd32c90c73a253d1848e72c394c8a3fe09a0c620fcb5ad17a38d15b96620fd504be6c7dd57f25a889e20a",
|
||||
"574556310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687fcb90cb92576cc567d08bedfa255bb94366c5b882a03ce247745dfd0e32ab09c010000000000000021509022cbf78272fd37df1a2ac940fddcacf6b7c394169b54e256abac734c8200093d000000000000040400040100001557491f18f4601eba695fd96f943ad21fe5e87575fe87a3824c41e81a2e99fd47231c68b03c133f8dd6a33001fe1fa77671adaf576efa3dad5ae35488ac6508"
|
||||
],
|
||||
[
|
||||
"574556310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a2036872df53b5dcc0c0ad294a87b904069ccadbde41494fa7e39cbf6720e1b2d5c5aa10000000000000000000000000000000000000000000000000000000000000000000000000000000080841e000000000000034300200057656c636f6d652e204920616d20616e206175746f6d6174656420686f73742e0287c8c0e35fc7fa6a9d4b19223f40282fc35e9fec2e59380ac5b3dfaeae6d590818cbffb02f3b3c5c74d381e071119923078b88c258f5dc2ed86b012166e80546d4a2111fe5e0e26a4e24428b73194386e0b4f1f731c50e47737760846cb1ea0f",
|
||||
"574556310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a2036872df53b5dcc0c0ad294a87b904069ccadbde41494fa7e39cbf6720e1b2d5c5aa1010000000000000081cef7e8b20d0491664ba49ee6fbd5f26e16ec4decbcf820a3f7dedb10ac0403c0c62d0000000000000a3200d00700000387c8c0e35fc7fa6a9d4b19223f40282fc35e9fec2e59380ac5b3dfaeae6d590802656e080057656c636f6d652ebeb6999762bad005e0998b47d5b193f5e32326601e9524604b5b39d2eb25fb631cd22f64c01ab4db57ccd4a104e810387806e200748bdce6cdeb0720a7c3280f"
|
||||
]
|
||||
],
|
||||
"outcome": "accept",
|
||||
"room": "91ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687",
|
||||
"grant_digests": [
|
||||
"3427f23d6605d4ed9be070c149aa749edd535e46fb4a813e47bdd750d546f965",
|
||||
"37d406a857ee63ae9fecd4f293211c3e92c6847beb4871066ecfbca21f4a3860"
|
||||
],
|
||||
"order": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
null
|
||||
],
|
||||
[
|
||||
0,
|
||||
1,
|
||||
null
|
||||
],
|
||||
[
|
||||
1,
|
||||
0,
|
||||
"37d406a857ee63ae9fecd4f293211c3e92c6847beb4871066ecfbca21f4a3860"
|
||||
],
|
||||
[
|
||||
2,
|
||||
0,
|
||||
"3427f23d6605d4ed9be070c149aa749edd535e46fb4a813e47bdd750d546f965"
|
||||
],
|
||||
[
|
||||
2,
|
||||
1,
|
||||
"3427f23d6605d4ed9be070c149aa749edd535e46fb4a813e47bdd750d546f965"
|
||||
],
|
||||
[
|
||||
1,
|
||||
1,
|
||||
"37d406a857ee63ae9fecd4f293211c3e92c6847beb4871066ecfbca21f4a3860"
|
||||
]
|
||||
],
|
||||
"state": {
|
||||
"present": [
|
||||
{
|
||||
"principal": "2df53b5dcc0c0ad294a87b904069ccadbde41494fa7e39cbf6720e1b2d5c5aa1",
|
||||
"grant": "3427f23d6605d4ed9be070c149aa749edd535e46fb4a813e47bdd750d546f965"
|
||||
},
|
||||
{
|
||||
"principal": "fcb90cb92576cc567d08bedfa255bb94366c5b882a03ce247745dfd0e32ab09c",
|
||||
"grant": "37d406a857ee63ae9fecd4f293211c3e92c6847beb4871066ecfbca21f4a3860"
|
||||
}
|
||||
],
|
||||
"published": [],
|
||||
"slots": [
|
||||
{
|
||||
"slot": "stage",
|
||||
"media": "video",
|
||||
"live": null,
|
||||
"standby": null,
|
||||
"precedence": null
|
||||
},
|
||||
{
|
||||
"slot": "main",
|
||||
"media": "video",
|
||||
"live": null,
|
||||
"standby": null,
|
||||
"precedence": null
|
||||
}
|
||||
],
|
||||
"tracks": [],
|
||||
"timeline": [
|
||||
{
|
||||
"principal": "2df53b5dcc0c0ad294a87b904069ccadbde41494fa7e39cbf6720e1b2d5c5aa1",
|
||||
"grant": "3427f23d6605d4ed9be070c149aa749edd535e46fb4a813e47bdd750d546f965",
|
||||
"t": 0
|
||||
},
|
||||
{
|
||||
"principal": "fcb90cb92576cc567d08bedfa255bb94366c5b882a03ce247745dfd0e32ab09c",
|
||||
"grant": "37d406a857ee63ae9fecd4f293211c3e92c6847beb4871066ecfbca21f4a3860",
|
||||
"t": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"tally": {
|
||||
"state_hash": "771b30268d4b5a93046d9e87dbbc10bae6fc76c4c2d5dde9071f801bfdb9a68d",
|
||||
"polls": []
|
||||
},
|
||||
"moqt": [
|
||||
{
|
||||
"grant": "3427f23d6605d4ed9be070c149aa749edd535e46fb4a813e47bdd750d546f965",
|
||||
"reval": 60,
|
||||
"seek": false,
|
||||
"scopes": [
|
||||
[
|
||||
[
|
||||
0
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
2,
|
||||
6
|
||||
],
|
||||
[
|
||||
"91ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687",
|
||||
"2df53b5dcc0c0ad294a87b904069ccadbde41494fa7e39cbf6720e1b2d5c5aa1",
|
||||
"audio",
|
||||
null
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
2,
|
||||
6
|
||||
],
|
||||
[
|
||||
"91ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687",
|
||||
"2df53b5dcc0c0ad294a87b904069ccadbde41494fa7e39cbf6720e1b2d5c5aa1",
|
||||
"video",
|
||||
null
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
6
|
||||
],
|
||||
[
|
||||
"91ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687",
|
||||
"_stage",
|
||||
"video",
|
||||
null
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
6
|
||||
],
|
||||
[
|
||||
"91ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687",
|
||||
"_stage",
|
||||
null
|
||||
],
|
||||
"catalog"
|
||||
],
|
||||
[
|
||||
[
|
||||
6
|
||||
],
|
||||
[
|
||||
"91ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687",
|
||||
"_main",
|
||||
"video",
|
||||
null
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
6
|
||||
],
|
||||
[
|
||||
"91ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687",
|
||||
"_main",
|
||||
null
|
||||
],
|
||||
"catalog"
|
||||
],
|
||||
[
|
||||
[
|
||||
2,
|
||||
6
|
||||
],
|
||||
[
|
||||
"91ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687",
|
||||
"2df53b5dcc0c0ad294a87b904069ccadbde41494fa7e39cbf6720e1b2d5c5aa1",
|
||||
"screen",
|
||||
null
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
2,
|
||||
6
|
||||
],
|
||||
[
|
||||
"91ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687",
|
||||
"2df53b5dcc0c0ad294a87b904069ccadbde41494fa7e39cbf6720e1b2d5c5aa1",
|
||||
null
|
||||
],
|
||||
"catalog"
|
||||
],
|
||||
[
|
||||
[
|
||||
2,
|
||||
6
|
||||
],
|
||||
[
|
||||
"91ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687",
|
||||
"2df53b5dcc0c0ad294a87b904069ccadbde41494fa7e39cbf6720e1b2d5c5aa1",
|
||||
"events",
|
||||
null
|
||||
],
|
||||
"0"
|
||||
],
|
||||
[
|
||||
[
|
||||
3
|
||||
],
|
||||
[
|
||||
"91ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
4,
|
||||
7,
|
||||
8
|
||||
],
|
||||
[
|
||||
"91ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687"
|
||||
],
|
||||
"catalog"
|
||||
],
|
||||
[
|
||||
[
|
||||
4,
|
||||
7,
|
||||
8
|
||||
],
|
||||
[
|
||||
"91ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687"
|
||||
],
|
||||
"0"
|
||||
],
|
||||
[
|
||||
[
|
||||
4,
|
||||
7,
|
||||
8
|
||||
],
|
||||
[
|
||||
"91ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687"
|
||||
],
|
||||
"0_receipts"
|
||||
],
|
||||
[
|
||||
[
|
||||
4,
|
||||
7,
|
||||
8
|
||||
],
|
||||
[
|
||||
"91ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687"
|
||||
],
|
||||
"1"
|
||||
],
|
||||
[
|
||||
[
|
||||
4,
|
||||
7,
|
||||
8
|
||||
],
|
||||
[
|
||||
"91ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687"
|
||||
],
|
||||
"1_receipts"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"grant": "37d406a857ee63ae9fecd4f293211c3e92c6847beb4871066ecfbca21f4a3860",
|
||||
"reval": 60,
|
||||
"seek": false,
|
||||
"scopes": [
|
||||
[
|
||||
[
|
||||
0
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
2,
|
||||
6
|
||||
],
|
||||
[
|
||||
"91ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687",
|
||||
"fcb90cb92576cc567d08bedfa255bb94366c5b882a03ce247745dfd0e32ab09c",
|
||||
"events",
|
||||
null
|
||||
],
|
||||
"0"
|
||||
],
|
||||
[
|
||||
[
|
||||
3
|
||||
],
|
||||
[
|
||||
"91ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
4,
|
||||
7,
|
||||
8
|
||||
],
|
||||
[
|
||||
"91ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687"
|
||||
],
|
||||
"catalog"
|
||||
],
|
||||
[
|
||||
[
|
||||
4,
|
||||
7,
|
||||
8
|
||||
],
|
||||
[
|
||||
"91ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687"
|
||||
],
|
||||
"0"
|
||||
],
|
||||
[
|
||||
[
|
||||
4,
|
||||
7,
|
||||
8
|
||||
],
|
||||
[
|
||||
"91ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687"
|
||||
],
|
||||
"0_receipts"
|
||||
],
|
||||
[
|
||||
[
|
||||
4,
|
||||
7,
|
||||
8
|
||||
],
|
||||
[
|
||||
"91ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687"
|
||||
],
|
||||
"1"
|
||||
],
|
||||
[
|
||||
[
|
||||
4,
|
||||
7,
|
||||
8
|
||||
],
|
||||
[
|
||||
"91ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687"
|
||||
],
|
||||
"1_receipts"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ai_unmarked",
|
||||
"about": "an AI participant's chat that states no origin (§9.2 step 6)",
|
||||
"descriptor": "5752443101988ec20b60d2d23c4c475c1f4e623f115ffbe7659bd0c81c0af9233c28589285c73c9d1dc51b9a83c180ec0bd17f1c8520c0ab70c4874225109cadb2d493ec1b00406cd8e95d060040420f0000000000150068747470733a2f2f726f6f6d732e6578616d706c650800000003003c0000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f00000000000504686f737407010102ae06000000000000000000000000000000000000000000000000000000000000096d6f64657261746f720001000108080000000000000000000000000000000000000000000000000000000000000861756469656e636500010100580000000000000000000000000000000000000000000000000000000000000005677565737403010100580100000000000000000000000000000000000000000000000000000000000007656e636f64657203000000000000000000000000000000000000000000000000000000000000000000000002057374616765020100000000046d61696e0202d007000002067370656563680101046f707573000001000000000000100000004000000080bb0000010f0a63616d6572612d37323002020d617630312e302e30354d2e30380b617663312e363430303146010002800268011e0096000000f40100000005d0021e00f4010000dc0500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000086c97e1a4b2609cb3438fd6c165e0bdc6f016bae7d16c4d37a387f4be3891b46f97c63fbf4814de11aadb3ee409d246a182017b43cacb6a6c64905bceb31c30b",
|
||||
"grants": [
|
||||
"575247310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a2036872df53b5dcc0c0ad294a87b904069ccadbde41494fa7e39cbf6720e1b2d5c5aa100000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a0000000000070101020000000000000000000000000000000000000000000000000000000000000000005745503101ae060000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011568747470733a2f2f72656c61792e6578616d706c6545000261694000383763386330653335666337666136613964346231393232336634303238326663333565396665633265353933383061633562336466616561653664353930385c245e4d368eea2fa46a37c069226e63a3489e51338486b574f405737169de74e0e230e5f27b523eb35cdc5e5a63fe4f104c48556ba46d8433b3db9a265eaa01",
|
||||
"575247310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687fcb90cb92576cc567d08bedfa255bb94366c5b882a03ce247745dfd0e32ab09c00000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a000000000200010100000000000000000000000000000000000000000000000000000000000000000000574550310158000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011568747470733a2f2f72656c61792e6578616d706c650000da52cb8a9a33184a5da85c37affe7d9dfc6f4e7a3019ecc07c090358ded448066ac88859718562bb805f34d7da43880addb6098e654e8e51481afb9ee22ee40e"
|
||||
],
|
||||
"key_log_head": "{\"grant_max_s\":86400,\"issued_at\":\"2026-10-15T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"room-authority\",\"key\":\"988ec20b60d2d23c4c475c1f4e623f115ffbe7659bd0c81c0af9233c28589285\",\"may_sign\":[\"live/room\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"3328656f3df5d9617e108d46080e23610712c9979d4437e706240f53d6cadf7b\",\"origin\":\"https://rooms.example\",\"prev\":null,\"revoked_modules\":[],\"root\":\"687972706312d9675734bf13b4bcf695ecb1b374de7809242aa91f130c6a1c0c\",\"seq\":0,\"sig\":\"ba55b157ebe88fc97f2b5a5e3556415563552ed88b9931060b9e99ae0a7decefdc63e2cb916b9a5d03c80a0c1823de14e7d8d3fe0b726c2464d823611780290a\",\"status_max_age_s\":3600}",
|
||||
"chains": [
|
||||
[
|
||||
"574556310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687c73c9d1dc51b9a83c180ec0bd17f1c8520c0ab70c4874225109cadb2d493ec1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001040002df53b5dcc0c0ad294a87b904069ccadbde41494fa7e39cbf6720e1b2d5c5aa13427f23d6605d4ed9be070c149aa749edd535e46fb4a813e47bdd750d546f9654022ff7403111b077255cc79fffeb37146f44c32b5f5f811ee202b6a1811fc80d874544e4e887d493655b3d5f1e26a16664ec23594e8e685b4201ff3d8c9e105",
|
||||
"574556310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687c73c9d1dc51b9a83c180ec0bd17f1c8520c0ab70c4874225109cadb2d493ec1b010000000000000066e0e16b1f2817739e3b6f2c9c87086b31f86249123017fc159142eca323af28000000000000000000104000fcb90cb92576cc567d08bedfa255bb94366c5b882a03ce247745dfd0e32ab09c37d406a857ee63ae9fecd4f293211c3e92c6847beb4871066ecfbca21f4a38607dad484d1fc66d48a45d08c1cec17d18889212fe90f204ee16684d701e5ebf4cb837bdd6dd0af53318a5465ac6f44a98f3769e7481e99169975af532a5614809"
|
||||
],
|
||||
[
|
||||
"574556310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687fcb90cb92576cc567d08bedfa255bb94366c5b882a03ce247745dfd0e32ab09c0000000000000000000000000000000000000000000000000000000000000000000000000000000040420f0000000000000312001000497320616e796f6e652074686572653f882e98ec64ee02bb24da475a9dc9c491bb077b9fbd4cd32c90c73a253d1848e72c394c8a3fe09a0c620fcb5ad17a38d15b96620fd504be6c7dd57f25a889e20a"
|
||||
],
|
||||
[
|
||||
"574556310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a2036872df53b5dcc0c0ad294a87b904069ccadbde41494fa7e39cbf6720e1b2d5c5aa10000000000000000000000000000000000000000000000000000000000000000000000000000000080841e000000000000032200200057656c636f6d652e204920616d20616e206175746f6d6174656420686f73742ed3025193682bf2a1635fed1f8919f6b62399702536a5bbb76d1658c9f00056b7d858882840895670a8c9ea39ecd2b96d2845d5e37481852523eec2e6a1bc4b0b"
|
||||
]
|
||||
],
|
||||
"outcome": "refuse",
|
||||
"reason": "ai_unmarked",
|
||||
"at": [
|
||||
"record",
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ai_unmarked_human",
|
||||
"about": "an AI participant's chat that states origin 1, human (§9.2 step 6)",
|
||||
"descriptor": "5752443101988ec20b60d2d23c4c475c1f4e623f115ffbe7659bd0c81c0af9233c28589285c73c9d1dc51b9a83c180ec0bd17f1c8520c0ab70c4874225109cadb2d493ec1b00406cd8e95d060040420f0000000000150068747470733a2f2f726f6f6d732e6578616d706c650800000003003c0000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f00000000000504686f737407010102ae06000000000000000000000000000000000000000000000000000000000000096d6f64657261746f720001000108080000000000000000000000000000000000000000000000000000000000000861756469656e636500010100580000000000000000000000000000000000000000000000000000000000000005677565737403010100580100000000000000000000000000000000000000000000000000000000000007656e636f64657203000000000000000000000000000000000000000000000000000000000000000000000002057374616765020100000000046d61696e0202d007000002067370656563680101046f707573000001000000000000100000004000000080bb0000010f0a63616d6572612d37323002020d617630312e302e30354d2e30380b617663312e363430303146010002800268011e0096000000f40100000005d0021e00f4010000dc0500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000086c97e1a4b2609cb3438fd6c165e0bdc6f016bae7d16c4d37a387f4be3891b46f97c63fbf4814de11aadb3ee409d246a182017b43cacb6a6c64905bceb31c30b",
|
||||
"grants": [
|
||||
"575247310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a2036872df53b5dcc0c0ad294a87b904069ccadbde41494fa7e39cbf6720e1b2d5c5aa100000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a0000000000070101020000000000000000000000000000000000000000000000000000000000000000005745503101ae060000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011568747470733a2f2f72656c61792e6578616d706c6545000261694000383763386330653335666337666136613964346231393232336634303238326663333565396665633265353933383061633562336466616561653664353930385c245e4d368eea2fa46a37c069226e63a3489e51338486b574f405737169de74e0e230e5f27b523eb35cdc5e5a63fe4f104c48556ba46d8433b3db9a265eaa01",
|
||||
"575247310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687fcb90cb92576cc567d08bedfa255bb94366c5b882a03ce247745dfd0e32ab09c00000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a000000000200010100000000000000000000000000000000000000000000000000000000000000000000574550310158000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011568747470733a2f2f72656c61792e6578616d706c650000da52cb8a9a33184a5da85c37affe7d9dfc6f4e7a3019ecc07c090358ded448066ac88859718562bb805f34d7da43880addb6098e654e8e51481afb9ee22ee40e"
|
||||
],
|
||||
"key_log_head": "{\"grant_max_s\":86400,\"issued_at\":\"2026-10-15T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"room-authority\",\"key\":\"988ec20b60d2d23c4c475c1f4e623f115ffbe7659bd0c81c0af9233c28589285\",\"may_sign\":[\"live/room\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"3328656f3df5d9617e108d46080e23610712c9979d4437e706240f53d6cadf7b\",\"origin\":\"https://rooms.example\",\"prev\":null,\"revoked_modules\":[],\"root\":\"687972706312d9675734bf13b4bcf695ecb1b374de7809242aa91f130c6a1c0c\",\"seq\":0,\"sig\":\"ba55b157ebe88fc97f2b5a5e3556415563552ed88b9931060b9e99ae0a7decefdc63e2cb916b9a5d03c80a0c1823de14e7d8d3fe0b726c2464d823611780290a\",\"status_max_age_s\":3600}",
|
||||
"chains": [
|
||||
[
|
||||
"574556310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687c73c9d1dc51b9a83c180ec0bd17f1c8520c0ab70c4874225109cadb2d493ec1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001040002df53b5dcc0c0ad294a87b904069ccadbde41494fa7e39cbf6720e1b2d5c5aa13427f23d6605d4ed9be070c149aa749edd535e46fb4a813e47bdd750d546f9654022ff7403111b077255cc79fffeb37146f44c32b5f5f811ee202b6a1811fc80d874544e4e887d493655b3d5f1e26a16664ec23594e8e685b4201ff3d8c9e105",
|
||||
"574556310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687c73c9d1dc51b9a83c180ec0bd17f1c8520c0ab70c4874225109cadb2d493ec1b010000000000000066e0e16b1f2817739e3b6f2c9c87086b31f86249123017fc159142eca323af28000000000000000000104000fcb90cb92576cc567d08bedfa255bb94366c5b882a03ce247745dfd0e32ab09c37d406a857ee63ae9fecd4f293211c3e92c6847beb4871066ecfbca21f4a38607dad484d1fc66d48a45d08c1cec17d18889212fe90f204ee16684d701e5ebf4cb837bdd6dd0af53318a5465ac6f44a98f3769e7481e99169975af532a5614809"
|
||||
],
|
||||
[
|
||||
"574556310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687fcb90cb92576cc567d08bedfa255bb94366c5b882a03ce247745dfd0e32ab09c0000000000000000000000000000000000000000000000000000000000000000000000000000000040420f0000000000000312001000497320616e796f6e652074686572653f882e98ec64ee02bb24da475a9dc9c491bb077b9fbd4cd32c90c73a253d1848e72c394c8a3fe09a0c620fcb5ad17a38d15b96620fd504be6c7dd57f25a889e20a"
|
||||
],
|
||||
[
|
||||
"574556310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a2036872df53b5dcc0c0ad294a87b904069ccadbde41494fa7e39cbf6720e1b2d5c5aa10000000000000000000000000000000000000000000000000000000000000000000000000000000080841e000000000000032300200057656c636f6d652e204920616d20616e206175746f6d6174656420686f73742e0127f048da20a70e3abec01ff3cdf9c9563b4585ccd45c192e7d28a8ae98773910f796c7ffea6a735359c3d05699e25365dc6cad4d3d12f531758f088ada112c00"
|
||||
]
|
||||
],
|
||||
"outcome": "refuse",
|
||||
"reason": "ai_unmarked",
|
||||
"at": [
|
||||
"record",
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ai_unmarked_model",
|
||||
"about": "an AI participant's chat that states origin 2 with a model other than its grant's (§9.2 step 6)",
|
||||
"descriptor": "5752443101988ec20b60d2d23c4c475c1f4e623f115ffbe7659bd0c81c0af9233c28589285c73c9d1dc51b9a83c180ec0bd17f1c8520c0ab70c4874225109cadb2d493ec1b00406cd8e95d060040420f0000000000150068747470733a2f2f726f6f6d732e6578616d706c650800000003003c0000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f00000000000504686f737407010102ae06000000000000000000000000000000000000000000000000000000000000096d6f64657261746f720001000108080000000000000000000000000000000000000000000000000000000000000861756469656e636500010100580000000000000000000000000000000000000000000000000000000000000005677565737403010100580100000000000000000000000000000000000000000000000000000000000007656e636f64657203000000000000000000000000000000000000000000000000000000000000000000000002057374616765020100000000046d61696e0202d007000002067370656563680101046f707573000001000000000000100000004000000080bb0000010f0a63616d6572612d37323002020d617630312e302e30354d2e30380b617663312e363430303146010002800268011e0096000000f40100000005d0021e00f4010000dc0500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000086c97e1a4b2609cb3438fd6c165e0bdc6f016bae7d16c4d37a387f4be3891b46f97c63fbf4814de11aadb3ee409d246a182017b43cacb6a6c64905bceb31c30b",
|
||||
"grants": [
|
||||
"575247310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a2036872df53b5dcc0c0ad294a87b904069ccadbde41494fa7e39cbf6720e1b2d5c5aa100000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a0000000000070101020000000000000000000000000000000000000000000000000000000000000000005745503101ae060000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011568747470733a2f2f72656c61792e6578616d706c6545000261694000383763386330653335666337666136613964346231393232336634303238326663333565396665633265353933383061633562336466616561653664353930385c245e4d368eea2fa46a37c069226e63a3489e51338486b574f405737169de74e0e230e5f27b523eb35cdc5e5a63fe4f104c48556ba46d8433b3db9a265eaa01",
|
||||
"575247310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687fcb90cb92576cc567d08bedfa255bb94366c5b882a03ce247745dfd0e32ab09c00000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a000000000200010100000000000000000000000000000000000000000000000000000000000000000000574550310158000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000011568747470733a2f2f72656c61792e6578616d706c650000da52cb8a9a33184a5da85c37affe7d9dfc6f4e7a3019ecc07c090358ded448066ac88859718562bb805f34d7da43880addb6098e654e8e51481afb9ee22ee40e"
|
||||
],
|
||||
"key_log_head": "{\"grant_max_s\":86400,\"issued_at\":\"2026-10-15T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"room-authority\",\"key\":\"988ec20b60d2d23c4c475c1f4e623f115ffbe7659bd0c81c0af9233c28589285\",\"may_sign\":[\"live/room\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"3328656f3df5d9617e108d46080e23610712c9979d4437e706240f53d6cadf7b\",\"origin\":\"https://rooms.example\",\"prev\":null,\"revoked_modules\":[],\"root\":\"687972706312d9675734bf13b4bcf695ecb1b374de7809242aa91f130c6a1c0c\",\"seq\":0,\"sig\":\"ba55b157ebe88fc97f2b5a5e3556415563552ed88b9931060b9e99ae0a7decefdc63e2cb916b9a5d03c80a0c1823de14e7d8d3fe0b726c2464d823611780290a\",\"status_max_age_s\":3600}",
|
||||
"chains": [
|
||||
[
|
||||
"574556310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687c73c9d1dc51b9a83c180ec0bd17f1c8520c0ab70c4874225109cadb2d493ec1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001040002df53b5dcc0c0ad294a87b904069ccadbde41494fa7e39cbf6720e1b2d5c5aa13427f23d6605d4ed9be070c149aa749edd535e46fb4a813e47bdd750d546f9654022ff7403111b077255cc79fffeb37146f44c32b5f5f811ee202b6a1811fc80d874544e4e887d493655b3d5f1e26a16664ec23594e8e685b4201ff3d8c9e105",
|
||||
"574556310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687c73c9d1dc51b9a83c180ec0bd17f1c8520c0ab70c4874225109cadb2d493ec1b010000000000000066e0e16b1f2817739e3b6f2c9c87086b31f86249123017fc159142eca323af28000000000000000000104000fcb90cb92576cc567d08bedfa255bb94366c5b882a03ce247745dfd0e32ab09c37d406a857ee63ae9fecd4f293211c3e92c6847beb4871066ecfbca21f4a38607dad484d1fc66d48a45d08c1cec17d18889212fe90f204ee16684d701e5ebf4cb837bdd6dd0af53318a5465ac6f44a98f3769e7481e99169975af532a5614809"
|
||||
],
|
||||
[
|
||||
"574556310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a203687fcb90cb92576cc567d08bedfa255bb94366c5b882a03ce247745dfd0e32ab09c0000000000000000000000000000000000000000000000000000000000000000000000000000000040420f0000000000000312001000497320616e796f6e652074686572653f882e98ec64ee02bb24da475a9dc9c491bb077b9fbd4cd32c90c73a253d1848e72c394c8a3fe09a0c620fcb5ad17a38d15b96620fd504be6c7dd57f25a889e20a"
|
||||
],
|
||||
[
|
||||
"574556310191ac4a45ebdee375b286d920fb2d07f064a31e7b7243829126eee2e27a2036872df53b5dcc0c0ad294a87b904069ccadbde41494fa7e39cbf6720e1b2d5c5aa10000000000000000000000000000000000000000000000000000000000000000000000000000000080841e000000000000034300200057656c636f6d652e204920616d20616e206175746f6d6174656420686f73742e0267fa6ab069abc245aeccbc6afa47448ff1510522c9f9b7a79b7da263115ac1813dad0dd58243a1930f8b515255419da8042fb3bd634162e4d49a0eaa3c634dcc81144fe2cc83c3d0852e7a516af18e7f9deaa4883b4faf13af1ae9f150590302"
|
||||
]
|
||||
],
|
||||
"outcome": "refuse",
|
||||
"reason": "ai_unmarked",
|
||||
"at": [
|
||||
"record",
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "rate_exceeded",
|
||||
"about": "a third event in one second under a grant of two per second (§4.1; live-events §6.3)",
|
||||
|
|
|
|||
38
wai/live-conformance/synthesis/README.md
Normal file
38
wai/live-conformance/synthesis/README.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# live-conformance/synthesis — synthesis provenance
|
||||
|
||||
The corpus of [`extensions/synthesis.md`](../../extensions/synthesis.md) §13, in one file,
|
||||
`vectors.json`: the EU icons §10.2 pins (`eu_icons`, each its `file` and `sha256`), and the
|
||||
**vectors**, each a case of §11.1 and what a verifier gives it.
|
||||
|
||||
A case holds its `policy` (`dual_layer`, `assistive`); its records as JSON texts, list by list:
|
||||
`claims` (origin claims), `markings`, `detections`, `reviews`, `label_policy` (a text or
|
||||
`null`) and `player` (the log's entries); the `objects` it holds, each a `WAI1` envelope in
|
||||
hexadecimal; the `decoded` samples it holds, each the content hash of the `object` decoded and
|
||||
its canonical `samples` in hexadecimal; and a `room` or `null`: its `descriptor`, `grants`,
|
||||
`key_log` and `chains` as live-rooms' corpus gives them.
|
||||
|
||||
**Synthesis equivalence.** For each vector, a conforming verifier reaches the outcome it states:
|
||||
|
||||
- `refuse`: the `reason` and where, `at`: `["claim", i]`, `["marking", i]`, `["detection", i]`,
|
||||
`["review", i]`, `["label_policy"]`, `["player", i]`, or `["room", …]` with the room verifier's
|
||||
own place (`["room", "record", c, i]` for record `i` of chain `c`);
|
||||
- `accept`: the `statements` of §11.3: `claims`, each its `link`, `class`, `source`, `c2pa` (the
|
||||
statement of §6, exactly as written) and `assistive` (`proved`, `not-recomputed`, `asserted`, or
|
||||
`null` for a claim that is not an assist); `detections`, each its `subject` and `result`;
|
||||
`ai_tracks`, each its `track` and `claim`; `reviews_presented`, each its log `entry` and
|
||||
`review`; `labels`, each its log `entry`, `trigger` and the `shown` entry that met it;
|
||||
`visual_icon`, the file of §10.2 the label policy's visual is, or `null`; and `room`, the room's
|
||||
own outcome or `null`.
|
||||
|
||||
A verifier that reaches another outcome, reason, place or statement does not conform.
|
||||
|
||||
**Provenance.** `wai_synthesis_vectors gen` writes `vectors.json` from records authored in code:
|
||||
every key is SHA-256 of the corpus's label and an index, every sample buffer and envelope payload
|
||||
is SHA-256 output of a label, every time is a constant, and Ed25519 signing is deterministic, so
|
||||
the file is the same bytes on every machine. The watermark scheme the markings and detections
|
||||
name, `wai-mark:conformance`, names no algorithm (§7.4): the corpus exercises the records and the
|
||||
agreement of the two layers, not a watermark.
|
||||
|
||||
```
|
||||
python3 -I ../synthesis_verify.py .
|
||||
```
|
||||
1552
wai/live-conformance/synthesis/vectors.json
Normal file
1552
wai/live-conformance/synthesis/vectors.json
Normal file
File diff suppressed because it is too large
Load diff
864
wai/live-conformance/synthesis_verify.py
Normal file
864
wai/live-conformance/synthesis_verify.py
Normal file
|
|
@ -0,0 +1,864 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Second implementation of extensions/synthesis.md (`wai.media.synthesis`), over the corpus
|
||||
`live-conformance/synthesis/`.
|
||||
|
||||
Written from the extension's text. It shares no code with wai-rs: it reads every record as §2
|
||||
has it read (strict JSON, the encodings of jwp-receipts §2.4.3), verifies every signature with
|
||||
WAI's strict Ed25519 rule (claim-conformance/_ed25519.py) and every link hash with BLAKE3
|
||||
written from its specification (claim-conformance/_blake3.py), runs the two pinned derivations
|
||||
of §4 in its own arithmetic, writes the digital source type of §5 and the C2PA statement of §6
|
||||
itself, and verifies each room with the live-rooms corpus's second implementation
|
||||
(live_rooms_verify.py). It reads §10.2's pinned icons and §12's codes from the extension.
|
||||
|
||||
python3 -I synthesis_verify.py <live-conformance/synthesis>
|
||||
|
||||
It gives every vector of the corpus the outcome, the reason, the place and the statements the
|
||||
corpus states, checks that every reason of §12 refuses a vector, and then mutates copies of the
|
||||
corpus and requires every copy to fail. Exit status 0 only if all of that holds. Python 3.8 or
|
||||
later; standard library only.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import unicodedata
|
||||
|
||||
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
|
||||
from _blake3 import blake3 # noqa: E402
|
||||
|
||||
EXTENSION = os.path.join(HERE, "..", "extensions", "synthesis.md")
|
||||
REGISTRY = os.path.join(HERE, "..", "registry", "capabilities.json")
|
||||
|
||||
SIGN_DOMAIN = b"wai:synthesis\x01" # §2
|
||||
LINK_DOMAIN = b"wai:synthesis-link\x01" # §2
|
||||
OBJECT_DOMAIN = b"moq-jwp:object\x01" # jwp-receipts §2.1
|
||||
IPTC = "http://cv.iptc.org/newscodes/digitalsourcetype/"
|
||||
MODEL_ID = "tag:transaction.science,2026:wai/model/" # §6
|
||||
UPSCALE2, DENOISE3 = "wai-synthesis:upscale2", "wai-synthesis:denoise3"
|
||||
MAX_INT = (1 << 53) - 1
|
||||
|
||||
CLASSES = ["captured", "reconstructed", "assistive", "synthesized"] # §3.2, in order
|
||||
STEP_CLASS = {"capture": "captured", "convert": "reconstructed", "assist": "assistive", "generate": "synthesized"}
|
||||
CAPTURE_SOURCES = ["digitalCapture", "computationalCapture", "screenCapture", "digitalCreation"]
|
||||
SOURCES = CAPTURE_SOURCES + ["algorithmicallyEnhanced", "trainedAlgorithmicMedia", "compositeWithTrainedAlgorithmicMedia",
|
||||
"algorithmicMedia", "composite", "compositeCapture", "compositeSynthetic"] # §5
|
||||
OVERSIGHT = ["fully_autonomous", "prompt_guided", "human_validated"]
|
||||
MEDIA = ["audio", "video", "image", "text"]
|
||||
PATH_TIERS = ["decode-equivalence", "generation-equivalence", "entropy-consistency", "none"]
|
||||
REVIEW_TIERS = ["decode-equivalence", "generation-equivalence"]
|
||||
|
||||
# Unicode's White_Space property (a term holds none of these, jwp-receipts §2.4.3).
|
||||
WHITE_SPACE = set("\t\n\x0b\x0c\r \x85\xa0 "
|
||||
"
")
|
||||
|
||||
|
||||
class Refuse(Exception):
|
||||
def __init__(self, reason, at=None):
|
||||
super().__init__(reason)
|
||||
self.reason, self.at = reason, at
|
||||
|
||||
|
||||
class Bad(Exception):
|
||||
"""A record that does not read: `record_malformed`."""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- §2 records
|
||||
|
||||
def strict_json(text):
|
||||
"""RFC 8259, no member named twice, no number but an unsigned integer, no unpaired
|
||||
surrogate, at most 32 levels."""
|
||||
def pairs(ps):
|
||||
d = {}
|
||||
for k, v in ps:
|
||||
if k in d:
|
||||
raise Bad("a member named twice")
|
||||
d[k] = v
|
||||
return d
|
||||
|
||||
def no_float(s):
|
||||
raise Bad("a number that is not an unsigned integer")
|
||||
|
||||
def integer(s):
|
||||
if s.startswith("-"):
|
||||
raise Bad("a negative number")
|
||||
return int(s)
|
||||
|
||||
try:
|
||||
v = json.loads(text, object_pairs_hook=pairs, parse_float=no_float, parse_int=integer, parse_constant=no_float)
|
||||
except (ValueError, RecursionError):
|
||||
raise Bad("not JSON")
|
||||
|
||||
def walk(x, depth):
|
||||
if isinstance(x, (dict, list)):
|
||||
if depth > 32:
|
||||
raise Bad("too deep")
|
||||
for y in (x.values() if isinstance(x, dict) else x):
|
||||
walk(y, depth + 1)
|
||||
if isinstance(x, dict):
|
||||
for k in x:
|
||||
walk(k, depth)
|
||||
elif isinstance(x, str):
|
||||
if any(0xD800 <= ord(c) <= 0xDFFF for c in x):
|
||||
raise Bad("an unpaired surrogate")
|
||||
elif isinstance(x, bool):
|
||||
raise Bad("a boolean")
|
||||
elif isinstance(x, int):
|
||||
if x > MAX_INT:
|
||||
raise Bad("an integer past 2^53 - 1")
|
||||
walk(v, 1)
|
||||
return v
|
||||
|
||||
|
||||
def jstr(s):
|
||||
out = ['"']
|
||||
for c in s:
|
||||
o = ord(c)
|
||||
if c == '"':
|
||||
out.append('\\"')
|
||||
elif c == "\\":
|
||||
out.append("\\\\")
|
||||
elif c == "\b":
|
||||
out.append("\\b")
|
||||
elif c == "\f":
|
||||
out.append("\\f")
|
||||
elif c == "\n":
|
||||
out.append("\\n")
|
||||
elif c == "\r":
|
||||
out.append("\\r")
|
||||
elif c == "\t":
|
||||
out.append("\\t")
|
||||
elif o < 0x20:
|
||||
out.append("\\u%04x" % o)
|
||||
else:
|
||||
out.append(c)
|
||||
out.append('"')
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def jcs(v):
|
||||
"""RFC 8785 on the values a record carries."""
|
||||
if v is None:
|
||||
return "null"
|
||||
if isinstance(v, bool):
|
||||
raise Bad("a boolean")
|
||||
if isinstance(v, int):
|
||||
return str(v)
|
||||
if isinstance(v, str):
|
||||
return jstr(v)
|
||||
if isinstance(v, list):
|
||||
return "[" + ",".join(jcs(x) for x in v) + "]"
|
||||
keys = sorted(v, key=lambda k: k.encode("utf-16-be"))
|
||||
return "{" + ",".join(jstr(k) + ":" + jcs(v[k]) for k in keys) + "}"
|
||||
|
||||
|
||||
def link_hash(v):
|
||||
return blake3(LINK_DOMAIN + jcs(v).encode("utf-8"))
|
||||
|
||||
|
||||
def signing_bytes(v):
|
||||
return SIGN_DOMAIN + jcs({k: x for k, x in v.items() if k != "sig"}).encode("utf-8")
|
||||
|
||||
|
||||
def need(cond):
|
||||
if not cond:
|
||||
raise Bad("a member breaks its rule")
|
||||
|
||||
|
||||
def hexb(v, n):
|
||||
need(isinstance(v, str) and len(v) == 2 * n and re.fullmatch(r"[0-9a-f]*", v) is not None)
|
||||
return bytes.fromhex(v)
|
||||
|
||||
|
||||
def opt_hex(v, n):
|
||||
return None if v is None else hexb(v, n)
|
||||
|
||||
|
||||
def integer(v):
|
||||
need(isinstance(v, int) and not isinstance(v, bool) and 0 <= v <= MAX_INT)
|
||||
return v
|
||||
|
||||
|
||||
def term(v):
|
||||
need(isinstance(v, str) and 0 < len(v.encode("utf-8")) <= 2048
|
||||
and not any(c in WHITE_SPACE or unicodedata.category(c) == "Cc" for c in v))
|
||||
return v
|
||||
|
||||
|
||||
def opt_term(v):
|
||||
return None if v is None else term(v)
|
||||
|
||||
|
||||
def one_of(v, values):
|
||||
need(isinstance(v, str) and v in values)
|
||||
return v
|
||||
|
||||
|
||||
def members(v, names):
|
||||
need(isinstance(v, dict) and set(v) == set(names))
|
||||
return v
|
||||
|
||||
|
||||
def date_time(s):
|
||||
"""RFC 3339 §5.6 date-time, with §5.7's days (jwp-receipts §2.4.3)."""
|
||||
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 or "")
|
||||
need(m is not None)
|
||||
y, mo, d, h, mi, sec = (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]
|
||||
need(1 <= mo <= 12 and 1 <= d <= days[mo - 1] and h <= 23 and mi <= 59 and sec <= 60)
|
||||
if m.group(9) is not None:
|
||||
need(int(m.group(9)) <= 23 and int(m.group(10)) <= 59)
|
||||
|
||||
|
||||
def read(text, kind, names, fields):
|
||||
"""§2: read one record, then its signature. `fields` reads the kind's own members."""
|
||||
try:
|
||||
v = strict_json(text)
|
||||
members(v, names)
|
||||
need(v["kind"] == kind)
|
||||
signer = hexb(v["signer_pubkey"], 32)
|
||||
sig = hexb(v["sig"], 64)
|
||||
if "issued_at" in v:
|
||||
date_time(v["issued_at"])
|
||||
r = fields(v)
|
||||
except (Bad, KeyError, TypeError, AttributeError):
|
||||
raise Refuse("record_malformed")
|
||||
bad = ED.verify(signer, signing_bytes(v), sig)
|
||||
if bad:
|
||||
raise Refuse(bad)
|
||||
r.update(value=v, signer=signer, link=link_hash(v))
|
||||
return r
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- §3 the origin claim
|
||||
|
||||
ORIGIN = ["kind", "subject", "media", "class", "step", "operation", "record", "source", "models", "oversight", "inputs",
|
||||
"evidence", "issued_at", "signer_pubkey", "sig"]
|
||||
|
||||
|
||||
def subject_of(v):
|
||||
t = v.get("type")
|
||||
if t == "object":
|
||||
members(v, ["type", "content_hash"])
|
||||
return ("object", hexb(v["content_hash"], 32))
|
||||
if t == "group":
|
||||
members(v, ["type", "receipt"])
|
||||
return ("group", hexb(v["receipt"], 32))
|
||||
if t == "track":
|
||||
members(v, ["type", "track", "from_group"])
|
||||
integer(v["from_group"])
|
||||
return ("track", hexb(v["track"], 32))
|
||||
raise Bad("a subject of no type")
|
||||
|
||||
|
||||
def origin_fields(v):
|
||||
c = {"subject": subject_of(v["subject"]), "media": one_of(v["media"], MEDIA), "class": one_of(v["class"], CLASSES),
|
||||
"step": one_of(v["step"], list(STEP_CLASS)), "operation": term(v["operation"]), "record": opt_hex(v["record"], 32),
|
||||
"source": one_of(v["source"], SOURCES),
|
||||
"oversight": None if v["oversight"] is None else one_of(v["oversight"], OVERSIGHT)}
|
||||
need(isinstance(v["models"], list) and len(v["models"]) <= 16)
|
||||
c["models"] = []
|
||||
for m in v["models"]:
|
||||
members(m, ["digest", "type"])
|
||||
c["models"].append({"digest": hexb(m["digest"], 32), "trained": one_of(m["type"], ["trained", "algorithmic"]) == "trained"})
|
||||
need(len({m["digest"] for m in c["models"]}) == len(c["models"]))
|
||||
need(isinstance(v["inputs"], list) and len(v["inputs"]) <= 16)
|
||||
c["inputs"] = []
|
||||
for i in v["inputs"]:
|
||||
members(i, ["content_hash", "origin", "class", "source"])
|
||||
c["inputs"].append({"content_hash": hexb(i["content_hash"], 32), "origin": opt_hex(i["origin"], 32),
|
||||
"class": one_of(i["class"], CLASSES), "source": one_of(i["source"], SOURCES)})
|
||||
need(len({i["content_hash"] for i in c["inputs"]}) == len(c["inputs"]))
|
||||
e = v["evidence"]
|
||||
if e is None:
|
||||
c["evidence"] = None
|
||||
else:
|
||||
members(e, ["tier", "derivation", "params", "width", "height", "input_samples", "output_samples"])
|
||||
need(isinstance(e["params"], dict))
|
||||
c["evidence"] = {"tier": one_of(e["tier"], ["replicate", "attest"]), "derivation": term(e["derivation"]),
|
||||
"params": e["params"], "width": integer(e["width"]), "height": integer(e["height"]),
|
||||
"input_samples": hexb(e["input_samples"], 32), "output_samples": hexb(e["output_samples"], 32)}
|
||||
need(c["evidence"]["width"] >= 1 and c["evidence"]["height"] >= 1)
|
||||
n, m, s = len(c["inputs"]), len(c["models"]), c["step"]
|
||||
plain = c["oversight"] is None and c["evidence"] is None
|
||||
if s == "capture":
|
||||
need(n == 0 and m == 0 and plain and c["source"] in CAPTURE_SOURCES)
|
||||
elif s == "convert":
|
||||
need(n >= 1 and m == 0 and plain)
|
||||
elif s == "assist":
|
||||
need(n == 1 and c["oversight"] is None and c["evidence"] is not None)
|
||||
else:
|
||||
need(m >= 1 and c["oversight"] is not None and c["evidence"] is None)
|
||||
return c
|
||||
|
||||
|
||||
def class_of(c):
|
||||
"""§3.2: the greatest of the step's class and the inputs'."""
|
||||
return max([STEP_CLASS[c["step"]]] + [i["class"] for i in c["inputs"]], key=CLASSES.index)
|
||||
|
||||
|
||||
def generated_source(c):
|
||||
"""§5."""
|
||||
if not any(m["trained"] for m in c["models"]):
|
||||
return "algorithmicMedia"
|
||||
if c["oversight"] == "human_validated" or any(i["class"] != "synthesized" for i in c["inputs"]):
|
||||
return "compositeWithTrainedAlgorithmicMedia"
|
||||
return "trainedAlgorithmicMedia"
|
||||
|
||||
|
||||
def source_of(c):
|
||||
"""§5's table."""
|
||||
ins = c["inputs"]
|
||||
if c["step"] == "capture":
|
||||
return c["source"]
|
||||
if c["step"] == "convert":
|
||||
if len(ins) == 1:
|
||||
return ins[0]["source"]
|
||||
if any(i["class"] == "synthesized" for i in ins):
|
||||
return "compositeSynthetic"
|
||||
if all(i["source"] in ("digitalCapture", "computationalCapture") for i in ins):
|
||||
return "compositeCapture"
|
||||
return "composite"
|
||||
if c["step"] == "assist":
|
||||
return ins[0]["source"] if ins[0]["class"] == "synthesized" else "algorithmicallyEnhanced"
|
||||
return generated_source(c)
|
||||
|
||||
|
||||
def statement(c):
|
||||
"""§6, as its JCS text."""
|
||||
if len(c["inputs"]) != 1:
|
||||
actions = [{"action": "c2pa.created", "digitalSourceType": IPTC + c["source"]}]
|
||||
elif c["step"] == "convert":
|
||||
actions = [{"action": "c2pa.opened"}, {"action": "c2pa.converted"}]
|
||||
elif c["step"] == "assist":
|
||||
actions = [{"action": "c2pa.opened"}, {"action": "c2pa.edited", "digitalSourceType": IPTC + "algorithmicallyEnhanced"}]
|
||||
else:
|
||||
actions = [{"action": "c2pa.opened"}, {"action": "c2pa.edited", "digitalSourceType": IPTC + generated_source(c)}]
|
||||
disclosures = []
|
||||
for m in [m for m in c["models"] if m["trained"]]:
|
||||
data = {"modelType": "c2pa.types.model", "modelIdentifier": MODEL_ID + m["digest"].hex()}
|
||||
if c["step"] == "generate":
|
||||
data["contentProfile"] = {"humanOversightLevel": c["oversight"]}
|
||||
k = len(disclosures)
|
||||
disclosures.append({"label": "c2pa.ai-disclosure" if k == 0 else "c2pa.ai-disclosure__%d" % k, "data": data})
|
||||
return jcs({"actions": actions, "assertions": disclosures})
|
||||
|
||||
|
||||
def payload_of(c):
|
||||
"""§7.2: the class's code, then seven bytes of the claim's link hash."""
|
||||
return bytes([CLASSES.index(c["class"])]) + c["link"][:7]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- §4 the derivations
|
||||
|
||||
def upscale2(px, w, h):
|
||||
out = bytearray(4 * w * h * 3)
|
||||
ow = 2 * w
|
||||
|
||||
def at(x, y, k):
|
||||
return px[(y * w + x) * 3 + k]
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
x1, y1 = min(x + 1, w - 1), min(y + 1, h - 1)
|
||||
for k in range(3):
|
||||
a, b, c, d = at(x, y, k), at(x1, y, k), at(x, y1, k), at(x1, y1, k)
|
||||
out[((2 * y) * ow + 2 * x) * 3 + k] = a
|
||||
out[((2 * y) * ow + 2 * x + 1) * 3 + k] = (a + b + 1) >> 1
|
||||
out[((2 * y + 1) * ow + 2 * x) * 3 + k] = (a + c + 1) >> 1
|
||||
out[((2 * y + 1) * ow + 2 * x + 1) * 3 + k] = (a + b + c + d + 2) >> 2
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def denoise3(px, w, h):
|
||||
out = bytearray(w * h * 3)
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
for k in range(3):
|
||||
s = 8
|
||||
for j in (-1, 0, 1):
|
||||
for i in (-1, 0, 1):
|
||||
weight = (2 if i == 0 else 1) * (2 if j == 0 else 1)
|
||||
xx, yy = min(max(x + i, 0), w - 1), min(max(y + j, 0), h - 1)
|
||||
s += weight * px[(yy * w + xx) * 3 + k]
|
||||
out[(y * w + x) * 3 + k] = s >> 4
|
||||
return bytes(out)
|
||||
|
||||
|
||||
DERIVATIONS = {UPSCALE2: (upscale2, 4), DENOISE3: (denoise3, 1)}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- §7, §9, §10 the other records
|
||||
|
||||
def marking_fields(v):
|
||||
return {"subject": hexb(v["subject"], 32), "origin": hexb(v["origin"], 32), "scheme": term(v["scheme"]),
|
||||
"scheme_digest": hexb(v["scheme_digest"], 32), "payload": hexb(v["payload"], 8)}
|
||||
|
||||
|
||||
def detection_fields(v):
|
||||
return {"subject": hexb(v["subject"], 32), "scheme": term(v["scheme"]), "scheme_digest": hexb(v["scheme_digest"], 32),
|
||||
"payload": opt_hex(v["payload"], 8)}
|
||||
|
||||
|
||||
def review_fields(v):
|
||||
verdict = one_of(v["verdict"], ["approved", "approved-with-notes", "rejected"])
|
||||
notes = opt_hex(v["notes"], 32)
|
||||
need((notes is None) == (verdict == "approved"))
|
||||
term(v["decoder"])
|
||||
opt_term(v["variant"])
|
||||
opt_term(v["role"])
|
||||
one_of(v["scope"], ["accuracy", "rendering", "translation"])
|
||||
return {"subject": hexb(v["subject"], 32), "origin": hexb(v["origin"], 32), "tier": one_of(v["tier"], REVIEW_TIERS),
|
||||
"reconstruction": hexb(v["reconstruction"], 32), "verdict": verdict}
|
||||
|
||||
|
||||
def policy_fields(v):
|
||||
need(isinstance(v["classes"], list) and len(v["classes"]) >= 1)
|
||||
idx = [CLASSES.index(one_of(c, CLASSES)) for c in v["classes"]]
|
||||
need(all(a < b for a, b in zip(idx, idx[1:])))
|
||||
interval = None if v["interval_us"] is None else integer(v["interval_us"])
|
||||
need(interval != 0)
|
||||
return {"classes": list(v["classes"]), "visual": hexb(v["visual"], 32), "audio": opt_hex(v["audio"], 32),
|
||||
"interval_us": interval, "grace_us": integer(v["grace_us"])}
|
||||
|
||||
|
||||
def entry_of(e):
|
||||
t = e.get("type")
|
||||
if t == "present":
|
||||
members(e, ["type", "subject", "origin", "class", "media", "tier", "decoder", "variant", "output"])
|
||||
term(e["decoder"])
|
||||
opt_term(e["variant"])
|
||||
return {"type": t, "subject": hexb(e["subject"], 32), "origin": opt_hex(e["origin"], 32),
|
||||
"class": None if e["class"] is None else one_of(e["class"], CLASSES), "media": one_of(e["media"], MEDIA),
|
||||
"tier": one_of(e["tier"], PATH_TIERS), "output": opt_hex(e["output"], 32)}
|
||||
if t == "shown":
|
||||
members(e, ["type", "label", "asset", "trigger"])
|
||||
return {"type": t, "audio": one_of(e["label"], ["visual", "audio"]) == "audio", "asset": hexb(e["asset"], 32),
|
||||
"trigger": one_of(e["trigger"], ["first-exposure", "resume", "interval"])}
|
||||
if t == "resume":
|
||||
members(e, ["type", "after"])
|
||||
one_of(e["after"], ["ad-break", "other"])
|
||||
return {"type": t}
|
||||
if t == "review":
|
||||
members(e, ["type", "subject", "review"])
|
||||
return {"type": t, "subject": hexb(e["subject"], 32), "review": hexb(e["review"], 32)}
|
||||
raise Bad("an entry of no type")
|
||||
|
||||
|
||||
def player_fields(v):
|
||||
return {"session": hexb(v["session"], 32), "seq": integer(v["seq"]), "prev": opt_hex(v["prev"], 32), "at": integer(v["at"]),
|
||||
"entry": entry_of(v["entry"])}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- §11 verification
|
||||
|
||||
def generates(env, classes):
|
||||
"""§3.3: an envelope whose capability's registry class is generative-exact or behavioral, or
|
||||
whose intent is create."""
|
||||
if len(env) < 8 or env[:4] != b"WAI1":
|
||||
return False
|
||||
n = int.from_bytes(env[4:8], "little")
|
||||
try:
|
||||
m = json.loads(env[8:8 + n])
|
||||
except ValueError:
|
||||
return False
|
||||
cap = m.get("model_requirement", {}).get("capability", "")
|
||||
return classes.get(cap) in ("generative-exact", "behavioral") or m.get("intent") == "create"
|
||||
|
||||
|
||||
def track_digest(room, owner, media, layer):
|
||||
"""live-rooms §6.2."""
|
||||
return hashlib.sha256(("-".join([room.hex(), owner, media]) + "\x00" + str(layer)).encode()).digest()
|
||||
|
||||
|
||||
def verify(case, classes):
|
||||
"""§11.2: the statements of §11.3, or Refuse with its reason and place."""
|
||||
st = {"claims": [], "detections": [], "ai_tracks": [], "reviews_presented": [], "labels": [], "visual_icon": None, "room": None}
|
||||
|
||||
def each(key, kind, names, fields, place):
|
||||
out = []
|
||||
for i, t in enumerate(case[key]):
|
||||
try:
|
||||
out.append(read(t, kind, names, fields))
|
||||
except Refuse as e:
|
||||
raise Refuse(e.reason, [place, i])
|
||||
return out
|
||||
|
||||
# Step 1.
|
||||
claims = each("claims", "wai-origin", ORIGIN, origin_fields, "claim")
|
||||
markings = each("markings", "wai-marking", ["kind", "subject", "origin", "scheme", "scheme_digest", "payload", "issued_at", "signer_pubkey", "sig"],
|
||||
marking_fields, "marking")
|
||||
detections = each("detections", "wai-detection", ["kind", "subject", "scheme", "scheme_digest", "payload", "issued_at", "signer_pubkey", "sig"],
|
||||
detection_fields, "detection")
|
||||
reviews = each("reviews", "wai-review", ["kind", "subject", "origin", "tier", "reconstruction", "decoder", "variant", "scope", "verdict", "notes",
|
||||
"role", "issued_at", "signer_pubkey", "sig"], review_fields, "review")
|
||||
policy = None
|
||||
if case["label_policy"] is not None:
|
||||
try:
|
||||
policy = read(case["label_policy"], "wai-label-policy", ["kind", "classes", "visual", "audio", "interval_us", "grace_us", "issued_at",
|
||||
"signer_pubkey", "sig"], policy_fields)
|
||||
except Refuse as e:
|
||||
raise Refuse(e.reason, ["label_policy"])
|
||||
log = each("player", "wai-player", ["kind", "session", "seq", "prev", "at", "entry", "signer_pubkey", "sig"], player_fields, "player")
|
||||
|
||||
by_link = {c["link"]: c for c in claims}
|
||||
objects = {blake3(OBJECT_DOMAIN + bytes.fromhex(o)): bytes.fromhex(o) for o in case["objects"]}
|
||||
decoded = {bytes.fromhex(d["object"]): bytes.fromhex(d["samples"]) for d in case["decoded"]}
|
||||
sha = lambda b: hashlib.sha256(b).digest() # noqa: E731
|
||||
|
||||
# Step 2.
|
||||
seen = set()
|
||||
for i, c in enumerate(claims):
|
||||
def no(reason):
|
||||
raise Refuse(reason, ["claim", i])
|
||||
key = jcs(c["value"]["subject"])
|
||||
if key in seen:
|
||||
no("claim_conflict")
|
||||
seen.add(key)
|
||||
if c["class"] != class_of(c):
|
||||
no("class_mismatch")
|
||||
if c["source"] != source_of(c):
|
||||
no("source_mismatch")
|
||||
for inp in c["inputs"]:
|
||||
held = by_link.get(inp["origin"])
|
||||
if held is not None and (held["subject"] != ("object", inp["content_hash"]) or held["class"] != inp["class"]
|
||||
or held["source"] != inp["source"]):
|
||||
no("input_mismatch")
|
||||
kind, h = c["subject"]
|
||||
if kind == "object" and h in objects and generates(objects[h], classes) and c["step"] != "generate":
|
||||
no("class_understated")
|
||||
status = None
|
||||
e = c["evidence"]
|
||||
if e is not None:
|
||||
w, hh = e["width"], e["height"]
|
||||
if e["tier"] == "replicate":
|
||||
if e["derivation"] not in DERIVATIONS or e["params"] != {} or DERIVATIONS[e["derivation"]][1] * w * hh * 3 > (1 << 26):
|
||||
no("evidence_invalid")
|
||||
status = "not-recomputed"
|
||||
px = decoded.get(c["inputs"][0]["content_hash"])
|
||||
if px is not None:
|
||||
if len(px) != w * hh * 3 or sha(px) != e["input_samples"]:
|
||||
no("input_samples_mismatch")
|
||||
if sha(DERIVATIONS[e["derivation"]][0](px, w, hh)) != e["output_samples"]:
|
||||
no("assistive_recompute_mismatch")
|
||||
status = "proved"
|
||||
else:
|
||||
status = "asserted"
|
||||
if kind == "object" and h in decoded and sha(decoded[h]) != e["output_samples"]:
|
||||
no("samples_mismatch")
|
||||
if case["policy"]["assistive"] == "require-proof" and status != "proved":
|
||||
no("assistive_not_proved")
|
||||
st["claims"].append({"link": c["link"].hex(), "class": c["class"], "source": c["source"], "c2pa": statement(c), "assistive": status})
|
||||
|
||||
# Step 3.
|
||||
for i, m in enumerate(markings):
|
||||
c = by_link.get(m["origin"])
|
||||
if c is None:
|
||||
raise Refuse("claim_unknown", ["marking", i])
|
||||
if c["subject"] != ("object", m["subject"]) or payload_of(c) != m["payload"]:
|
||||
raise Refuse("marking_mismatch", ["marking", i])
|
||||
for i, d in enumerate(detections):
|
||||
claim = next((c for c in claims if c["subject"] == ("object", d["subject"])), None)
|
||||
marking = next((m for m in markings if m["subject"] == d["subject"]), None)
|
||||
if marking is not None and (marking["scheme"], marking["scheme_digest"]) != (d["scheme"], d["scheme_digest"]):
|
||||
raise Refuse("detection_mismatch", ["detection", i])
|
||||
if d["payload"] is None:
|
||||
if marking is not None:
|
||||
raise Refuse("watermark_absent", ["detection", i])
|
||||
result = "unmarked"
|
||||
elif claim is None:
|
||||
result = "not-checked"
|
||||
elif d["payload"] != payload_of(claim):
|
||||
raise Refuse("layers_disagree", ["detection", i])
|
||||
else:
|
||||
result = "agree"
|
||||
st["detections"].append({"subject": d["subject"].hex(), "result": result})
|
||||
if case["policy"]["dual_layer"] == "required":
|
||||
for i, c in enumerate(claims):
|
||||
if c["class"] == "synthesized" and c["subject"][0] == "object" and not any(m["origin"] == c["link"] for m in markings):
|
||||
raise Refuse("layer_missing", ["claim", i])
|
||||
|
||||
# Step 4.
|
||||
for i, r in enumerate(reviews):
|
||||
c = by_link.get(r["origin"])
|
||||
if c is None:
|
||||
raise Refuse("claim_unknown", ["review", i])
|
||||
if c["subject"] != ("object", r["subject"]) or c["class"] != "synthesized":
|
||||
raise Refuse("review_subject_mismatch", ["review", i])
|
||||
|
||||
# Step 5.
|
||||
room = case["room"]
|
||||
if room is not None:
|
||||
chains = [[bytes.fromhex(e) for e in ch] for ch in room["chains"]]
|
||||
grants = [bytes.fromhex(g) for g in room["grants"]]
|
||||
out = LRV.verify_room(bytes.fromhex(room["descriptor"]), grants, room["key_log"], chains)
|
||||
if out["outcome"] == "refuse":
|
||||
raise Refuse(out["reason"], ["room"] + list(out["at"]))
|
||||
st["room"] = out["outcome"]
|
||||
d = LRV.read_descriptor(bytes.fromhex(room["descriptor"]))
|
||||
held, _ = LRV.check_grants(d, grants)
|
||||
by_digest = {g["digest"]: g for g in held}
|
||||
current = {}
|
||||
for c_i, r_i, under in out["order"]:
|
||||
if under is not None:
|
||||
continue
|
||||
ev = LEV.read_record(chains[c_i][r_i])
|
||||
if not 0x10 <= ev["kind"] <= 0x16:
|
||||
continue
|
||||
subject, x = ev["payload"][:32], LRV.read_detail(ev["kind"], ev["payload"][32:])
|
||||
if ev["kind"] == 0x10:
|
||||
current[subject] = x["grant"]
|
||||
continue
|
||||
if ev["kind"] == 0x14:
|
||||
current[subject] = x["new"]
|
||||
continue
|
||||
if ev["kind"] == 0x11:
|
||||
current.pop(subject, None)
|
||||
continue
|
||||
if ev["kind"] not in (0x12, 0x15):
|
||||
continue
|
||||
g = by_digest.get(current.get(subject))
|
||||
model = None
|
||||
for k, v in (LRV.read_attrs(g["attrs"]) or []) if g else []:
|
||||
if k == "ai":
|
||||
model = bytes.fromhex(v)
|
||||
if model is None:
|
||||
continue
|
||||
owner = subject.hex() if not x["slot"] else "_" + x["slot"]
|
||||
media = {1: "audio", 2: "video", 3: "screen"}[x["media"]]
|
||||
for layer in range(x["layers"]):
|
||||
t = track_digest(d["room"], owner, media, layer)
|
||||
found = next((c for c in claims if c["subject"] == ("track", t) and c["class"] == "synthesized" and c["step"] == "generate"
|
||||
and any(m["digest"] == model for m in c["models"])
|
||||
and (c["signer"] == subject or LRV.is_room_key(d, c["signer"]))), None)
|
||||
if found is None:
|
||||
raise Refuse("ai_track_unlabelled", ["room", "record", c_i, r_i])
|
||||
st["ai_tracks"].append({"track": t.hex(), "claim": found["link"].hex()})
|
||||
|
||||
# Step 6.
|
||||
for j, p in enumerate(log):
|
||||
ok = p["seq"] == j and p["session"] == log[0]["session"] and p["signer"] == log[0]["signer"]
|
||||
ok = ok and (p["prev"] is None if j == 0 else (p["prev"] == log[j - 1]["link"] and p["at"] >= log[j - 1]["at"]))
|
||||
if not ok:
|
||||
raise Refuse("player_chain_broken", ["player", j])
|
||||
for j, p in enumerate(log):
|
||||
e = p["entry"]
|
||||
if e["type"] == "present":
|
||||
c = by_link.get(e["origin"]) if e["origin"] is not None else None
|
||||
if c is not None and (c["subject"] != ("object", e["subject"]) or e["class"] != c["class"]):
|
||||
raise Refuse("present_mismatch", ["player", j])
|
||||
if e["output"] is not None and e["subject"] in decoded and sha(decoded[e["subject"]]) != e["output"]:
|
||||
raise Refuse("present_output_irreproducible", ["player", j])
|
||||
elif e["type"] == "review":
|
||||
r = next((r for r in reviews if r["link"] == e["review"]), None)
|
||||
if r is None:
|
||||
raise Refuse("claim_unknown", ["player", j])
|
||||
last = next((q["entry"] for q in reversed(log[:j]) if q["entry"]["type"] == "present" and q["entry"]["subject"] == e["subject"]), None)
|
||||
if last is None or r["subject"] != e["subject"]:
|
||||
raise Refuse("review_subject_mismatch", ["player", j])
|
||||
if r["verdict"] == "rejected":
|
||||
raise Refuse("review_rejected", ["player", j])
|
||||
if last["tier"] != r["tier"]:
|
||||
raise Refuse("review_path_not_exact", ["player", j])
|
||||
if last["output"] != r["reconstruction"]:
|
||||
raise Refuse("review_reconstruction_mismatch", ["player", j])
|
||||
st["reviews_presented"].append({"entry": j, "review": e["review"].hex()})
|
||||
if policy is not None:
|
||||
st["visual_icon"] = ICONS.get(policy["visual"].hex())
|
||||
for j, p in enumerate(log):
|
||||
e = p["entry"]
|
||||
if e["type"] == "shown" and (policy["audio"] if e["audio"] else policy["visual"]) != e["asset"]:
|
||||
raise Refuse("label_asset_mismatch", ["player", j])
|
||||
|
||||
def shown_in(audio, lo, hi):
|
||||
return next((k for k, q in enumerate(log) if q["entry"]["type"] == "shown" and q["entry"]["audio"] == audio and lo <= q["at"] <= hi), None)
|
||||
|
||||
last = {False: None, True: None}
|
||||
for j, p in enumerate(log):
|
||||
e = p["entry"]
|
||||
if e["type"] != "present" or e["class"] not in policy["classes"]:
|
||||
continue
|
||||
audio = e["media"] == "audio"
|
||||
hi = p["at"] + policy["grace_us"]
|
||||
prev = last[audio]
|
||||
if prev is None:
|
||||
trigger, k = "first-exposure", shown_in(audio, 0, hi)
|
||||
else:
|
||||
resumes = [q["at"] for q in log[prev + 1:j] if q["entry"]["type"] == "resume"]
|
||||
if resumes:
|
||||
trigger, k = "resume", shown_in(audio, resumes[-1], hi)
|
||||
elif policy["interval_us"] is not None:
|
||||
trigger, k = "interval", shown_in(audio, max(p["at"] - policy["interval_us"], 0), hi)
|
||||
else:
|
||||
last[audio] = j
|
||||
continue
|
||||
if k is None:
|
||||
raise Refuse("label_not_shown", ["player", j])
|
||||
st["labels"].append({"entry": j, "trigger": trigger, "shown": k})
|
||||
last[audio] = j
|
||||
return st
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- the extension's tables
|
||||
|
||||
def section(text, head):
|
||||
at = text.index("\n" + head)
|
||||
nxt = re.search(r"\n#{2,3} ", text[at + 1:])
|
||||
return text[at: at + 1 + nxt.start()] if nxt else text[at:]
|
||||
|
||||
|
||||
with open(EXTENSION, encoding="utf-8") as _f:
|
||||
_TEXT = _f.read()
|
||||
ICONS = {}
|
||||
for _line in section(_TEXT, "### 10.2 ").splitlines():
|
||||
_m = re.fullmatch(r"\| `(LABEL_AI[^`]*)` \| `([0-9a-f]{64})` \|", _line.strip())
|
||||
if _m:
|
||||
ICONS[_m.group(2)] = _m.group(1)
|
||||
REASONS = [l.split("`")[1] for l in section(_TEXT, "## 12. Reasons").splitlines() if l.startswith("| `")]
|
||||
|
||||
|
||||
def outcome(v, classes):
|
||||
try:
|
||||
return {"outcome": "accept", "statements": verify(v, classes)}
|
||||
except Refuse as e:
|
||||
return {"outcome": "refuse", "reason": e.reason, "at": e.at}
|
||||
|
||||
|
||||
def stated(v):
|
||||
return {k: v[k] for k in ("outcome", "reason", "at", "statements") if k in v}
|
||||
|
||||
|
||||
def check(doc, classes):
|
||||
"""Failures of a corpus document: an outcome other than the one stated, or a table that
|
||||
differs from the extension's."""
|
||||
fails = []
|
||||
pins = {d["sha256"]: d["file"] for d in doc["eu_icons"]}
|
||||
if pins != ICONS or len(doc["eu_icons"]) != len(ICONS):
|
||||
fails.append("the corpus's EU icons differ from §10.2's table")
|
||||
for v in doc["vectors"]:
|
||||
try:
|
||||
got = outcome(v, classes)
|
||||
except Exception as e: # noqa: BLE001 — a malformed copy fails, whatever it breaks
|
||||
got = {"outcome": "unreadable: %r" % (e,)}
|
||||
if got != stated(v):
|
||||
fails.append("%s: %s" % (v["name"], json.dumps(got)[:300]))
|
||||
return fails
|
||||
|
||||
|
||||
def flip_hex_digit(s, at):
|
||||
c = "0" if s[at] != "0" else "1"
|
||||
return s[:at] + c + s[at + 1:]
|
||||
|
||||
|
||||
def mutations(doc):
|
||||
"""Copies of the corpus, each with one defect a conforming verifier must catch."""
|
||||
vn = {v["name"]: k for k, v in enumerate(doc["vectors"])}
|
||||
|
||||
def mutate(label, f):
|
||||
d = copy.deepcopy(doc)
|
||||
f(d["vectors"])
|
||||
return label, d
|
||||
|
||||
chain, gen, room, cert = vn["chain"], vn["generated"], vn["ai_room"], vn["certified"]
|
||||
|
||||
def retext(vs, i, key, k, f):
|
||||
"""Rewrite record k of list `key` of vector i as JSON, applying f to its value."""
|
||||
v = json.loads(vs[i][key][k])
|
||||
f(v)
|
||||
vs[i][key][k] = json.dumps(v)
|
||||
|
||||
def decoded_byte(vs):
|
||||
d = vs[chain]["decoded"][0]
|
||||
d["samples"] = flip_hex_digit(d["samples"], 0)
|
||||
|
||||
def upscale_digit(vs):
|
||||
retext(vs, chain, "claims", 2, lambda v: v["evidence"].update(output_samples=flip_hex_digit(v["evidence"]["output_samples"], 5)))
|
||||
|
||||
def detection_class(vs):
|
||||
retext(vs, gen, "detections", 0, lambda v: v.update(payload="00" + v["payload"][2:]))
|
||||
|
||||
return [
|
||||
mutate("a digit of the upscale's signature", lambda vs: retext(vs, chain, "claims", 2, lambda v: v.update(sig=flip_hex_digit(v["sig"], 9)))),
|
||||
mutate("the upscale's output digest changed", upscale_digit),
|
||||
mutate("a byte of the reconstruction's decoded samples", decoded_byte),
|
||||
mutate("the reconstruction's claim withheld from the chain", lambda vs: vs[chain]["claims"].pop(1)),
|
||||
mutate("the generated video's detection reading class captured", detection_class),
|
||||
mutate("the generated video's marking withheld", lambda vs: vs[gen]["markings"].pop(0)),
|
||||
mutate("the composite's source stated compositeCapture",
|
||||
lambda vs: vs[gen]["statements"]["claims"][4].update(source="compositeCapture")),
|
||||
mutate("the transcode's statement stated as created",
|
||||
lambda vs: vs[gen]["statements"]["claims"][5].update(c2pa=vs[gen]["statements"]["claims"][1]["c2pa"])),
|
||||
mutate("the AI host's track claim withheld", lambda vs: vs[room].update(claims=[])),
|
||||
mutate("the AI host's grant withheld from the room", lambda vs: vs[room]["room"]["grants"].pop(0)),
|
||||
mutate("the certified log's first label withheld", lambda vs: vs[cert]["player"].pop(0)),
|
||||
mutate("the label after the break stated as an interval",
|
||||
lambda vs: vs[cert]["statements"]["labels"][1].update(trigger="interval")),
|
||||
mutate("the review stated at generation-equivalence",
|
||||
lambda vs: retext(vs, cert, "reviews", 0, lambda v: v.update(tier="generation-equivalence"))),
|
||||
mutate("the presented video's decoded frame changed",
|
||||
lambda vs: vs[cert]["decoded"][0].update(samples=flip_hex_digit(vs[cert]["decoded"][0]["samples"], 3))),
|
||||
mutate("the policy's visual stated as another pinned icon",
|
||||
lambda vs: vs[cert]["statements"].update(visual_icon="LABEL_AI_black.svg")),
|
||||
mutate("the first refusal stated at another place", lambda vs: vs[next(k for k, v in enumerate(vs) if v["outcome"] == "refuse")].update(at=["claim", 1])),
|
||||
("an EU icon's pin changed", dict(copy.deepcopy(doc), eu_icons=[dict(doc["eu_icons"][0], sha256="00" * 32)] + doc["eu_icons"][1:])),
|
||||
]
|
||||
|
||||
|
||||
def main(corpus_dir):
|
||||
fails = []
|
||||
st = ED._self_test()
|
||||
print(("PASS" if not st else "FAIL") + " Ed25519 strict rule self-test " + ",".join(st))
|
||||
if st:
|
||||
fails.append("ed25519 self-test")
|
||||
with open(REGISTRY, encoding="utf-8") as f:
|
||||
classes = {c["capability"]: c["replicate"] for c in json.load(f)["capabilities"]}
|
||||
with open(os.path.join(corpus_dir, "vectors.json"), encoding="utf-8") as f:
|
||||
doc = json.load(f)
|
||||
if len(ICONS) != 24:
|
||||
fails.append("§10.2's table does not read as 24 pinned icons")
|
||||
reasons_seen = set()
|
||||
for v in doc["vectors"]:
|
||||
try:
|
||||
got = outcome(v, classes)
|
||||
except Exception as e: # noqa: BLE001
|
||||
got = {"outcome": "unreadable: %r" % (e,)}
|
||||
ok = got == stated(v)
|
||||
if v.get("reason"):
|
||||
reasons_seen.add(v["reason"])
|
||||
print(("PASS" if ok else "FAIL") + " vector %s: %s" % (v["name"], got["outcome"] + (" %s at %s" % (got["reason"], got["at"]) if "reason" in got else "")))
|
||||
if not ok:
|
||||
fails.append("%s: %s, the corpus states %s" % (v["name"], json.dumps(got)[:400], json.dumps(stated(v))[:400]))
|
||||
table = check(doc, classes)
|
||||
if any("EU icons" in t for t in table):
|
||||
fails.append("the corpus's EU icons differ from §10.2's table")
|
||||
missing = [r for r in REASONS if r not in reasons_seen]
|
||||
if missing:
|
||||
fails.append("no vector refused with %s" % ", ".join(missing))
|
||||
accepted = {v["name"] for v in doc["vectors"] if v["outcome"] == "accept"}
|
||||
want = {"chain", "assistive_asserted", "generated", "ai_room", "certified"}
|
||||
if not want <= accepted:
|
||||
fails.append("vectors not accepted: %s" % ", ".join(sorted(want - accepted)))
|
||||
print("%s every reason of §12 refuses a vector, and every case of §13 is accepted" % ("PASS" if not missing and want <= accepted else "FAIL"))
|
||||
for label, d in mutations(doc):
|
||||
caught = check(d, classes)
|
||||
print(("PASS" if caught else "FAIL") + " mutated copy fails: %s%s" % (label, " (%s)" % caught[0][:100] if caught else ""))
|
||||
if not caught:
|
||||
fails.append("a mutated copy verifies: %s" % label)
|
||||
for f in fails:
|
||||
print("FAIL " + f)
|
||||
print(("PASS" if not fails else "FAIL") + " synthesis corpus: %d vectors" % len(doc["vectors"]))
|
||||
return 1 if fails else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print("usage: synthesis_verify.py <live-conformance/synthesis>", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
sys.exit(main(sys.argv[1]))
|
||||
|
|
@ -770,6 +770,20 @@
|
|||
"edition": null,
|
||||
"registered": "2026-10-20"
|
||||
},
|
||||
{
|
||||
"capability": "wai.media.synthesis",
|
||||
"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.mesh.draco",
|
||||
"kind": "payload",
|
||||
|
|
@ -2319,6 +2333,13 @@
|
|||
"section": "§5.1",
|
||||
"status": "defined"
|
||||
},
|
||||
{
|
||||
"name": "wai-detection",
|
||||
"kind": "document-kind",
|
||||
"document": "extensions/synthesis.md",
|
||||
"section": "§7.3",
|
||||
"status": "defined"
|
||||
},
|
||||
{
|
||||
"name": "wai-key-status",
|
||||
"kind": "document-kind",
|
||||
|
|
@ -2326,6 +2347,20 @@
|
|||
"section": "§7.9",
|
||||
"status": "defined"
|
||||
},
|
||||
{
|
||||
"name": "wai-label-policy",
|
||||
"kind": "document-kind",
|
||||
"document": "extensions/synthesis.md",
|
||||
"section": "§10.1",
|
||||
"status": "defined"
|
||||
},
|
||||
{
|
||||
"name": "wai-marking",
|
||||
"kind": "document-kind",
|
||||
"document": "extensions/synthesis.md",
|
||||
"section": "§7.2",
|
||||
"status": "defined"
|
||||
},
|
||||
{
|
||||
"name": "wai-module-grant",
|
||||
"kind": "document-kind",
|
||||
|
|
@ -2340,6 +2375,20 @@
|
|||
"section": "§8.4",
|
||||
"status": "defined"
|
||||
},
|
||||
{
|
||||
"name": "wai-origin",
|
||||
"kind": "document-kind",
|
||||
"document": "extensions/synthesis.md",
|
||||
"section": "§3.1",
|
||||
"status": "defined"
|
||||
},
|
||||
{
|
||||
"name": "wai-player",
|
||||
"kind": "document-kind",
|
||||
"document": "extensions/synthesis.md",
|
||||
"section": "§10.3",
|
||||
"status": "defined"
|
||||
},
|
||||
{
|
||||
"name": "wai-receipt-index",
|
||||
"kind": "document-kind",
|
||||
|
|
@ -2354,6 +2403,13 @@
|
|||
"section": "§4.1",
|
||||
"status": "defined"
|
||||
},
|
||||
{
|
||||
"name": "wai-review",
|
||||
"kind": "document-kind",
|
||||
"document": "extensions/synthesis.md",
|
||||
"section": "§9.1",
|
||||
"status": "defined"
|
||||
},
|
||||
{
|
||||
"name": "wai1",
|
||||
"kind": "codecs-prefix",
|
||||
|
|
|
|||
|
|
@ -618,6 +618,13 @@ wai:wai.live.watermark a wai:Capability ;
|
|||
wai:specSection "§5 Interactive, temporal & computed content" ;
|
||||
wai:registeredOn "2026-10-20"^^xsd:date .
|
||||
|
||||
wai:wai.media.synthesis a wai:Capability ;
|
||||
rdfs:label "wai.media.synthesis" ;
|
||||
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.mesh.draco a wai:Capability ;
|
||||
rdfs:label "wai.mesh.draco" ;
|
||||
wai:kind wai:kind-payload ;
|
||||
|
|
|
|||
|
|
@ -451,7 +451,8 @@ fn payload_json(ev: &Event) -> Value {
|
|||
"lang": c.lang,
|
||||
"text": c.text,
|
||||
}),
|
||||
Payload::Chat { text } => json!({"text": text}),
|
||||
Payload::Chat { text, origin: None } => json!({"text": text}),
|
||||
Payload::Chat { text, origin: Some(o) } => json!({"text": text, "origin": o.origin, "model": o.model.map(|m| hex(&m))}),
|
||||
Payload::Marker(m) => json!({"marker": format!("{m:?}")}),
|
||||
Payload::Reaction(r) => json!({"reaction": format!("{r:?}")}),
|
||||
Payload::Lifecycle { kind: k, subject, detail } => {
|
||||
|
|
@ -535,7 +536,7 @@ pub fn caption_payload(duration_ms: u32, origin: u8, model: Option<String>, lang
|
|||
|
||||
#[wasm_bindgen(js_name = chatPayload)]
|
||||
pub fn chat_payload(text: String) -> Vec<u8> {
|
||||
Payload::Chat { text }.encode()
|
||||
Payload::Chat { text, origin: None }.encode()
|
||||
}
|
||||
|
||||
/// A lifecycle payload (live-rooms §7.2) from its JSON description: `kind` (`join`,
|
||||
|
|
|
|||
|
|
@ -289,6 +289,15 @@ png_still = []
|
|||
# logs key_log's (key_discovery), its clips int_video's (video_int); jpeg-encoder, already a
|
||||
# dependency of `codecs`, writes its attested JPEG outputs. No new dependency.
|
||||
derive_edge = ["key_discovery", "video_int", "png_still", "dep:jpeg-encoder"]
|
||||
# Synthesis provenance (extensions/synthesis): `wai.media.synthesis` — the origin claim of an
|
||||
# object, a group or a track (captured, reconstructed, assistive or synthesized; the class rises
|
||||
# along a chain of derivations and never falls), its IPTC digital source type and C2PA statement,
|
||||
# the two pinned integer derivations whose recomputation proves an assistive claim, the marking
|
||||
# and detection records of the second marking layer and their agreement with the claim, the
|
||||
# tracks of a room's AI participants, reviews that a player presents only on a decode path of
|
||||
# the review's tier, and the label obligations of a player's signed log. SHA-256, BLAKE3,
|
||||
# Ed25519 and the key log only; no new dependency.
|
||||
synthesis = ["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,
|
||||
|
|
@ -743,6 +752,11 @@ name = "wai_live_moderation_vectors"
|
|||
path = "src/bin/wai_live_moderation_vectors.rs"
|
||||
required-features = ["live_moderation"]
|
||||
|
||||
[[bin]]
|
||||
name = "wai_synthesis_vectors"
|
||||
path = "src/bin/wai_synthesis_vectors.rs"
|
||||
required-features = ["synthesis"]
|
||||
|
||||
[[bin]]
|
||||
name = "wai_live_watermark_vectors"
|
||||
path = "src/bin/wai_live_watermark_vectors.rs"
|
||||
|
|
@ -1461,6 +1475,11 @@ name = "live_moderation"
|
|||
path = "tests/live_moderation.rs"
|
||||
required-features = ["live_moderation"]
|
||||
|
||||
[[test]]
|
||||
name = "synthesis"
|
||||
path = "tests/synthesis.rs"
|
||||
required-features = ["synthesis"]
|
||||
|
||||
[[test]]
|
||||
name = "live_watermark"
|
||||
path = "tests/live_watermark.rs"
|
||||
|
|
|
|||
74
wai/wai-rs/src/bin/wai_synthesis_vectors.rs
Normal file
74
wai/wai-rs/src/bin/wai_synthesis_vectors.rs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
//! Conformance corpus for synthesis provenance (`live-conformance/synthesis/`, extensions/synthesis.md §13).
|
||||
//!
|
||||
//! wai_synthesis_vectors gen <corpus-dir> write vectors.json from fixed seeds
|
||||
//! wai_synthesis_vectors verify <corpus-dir> 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/synthesis_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::synthesis::corpus;
|
||||
|
||||
fn main() {
|
||||
let a: Vec<String> = 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_synthesis_vectors gen|verify <corpus-dir>");
|
||||
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<String, String> = 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
|
||||
}
|
||||
|
|
@ -417,7 +417,7 @@ fn write(s: &Scene) -> Written {
|
|||
rm.emit(DIRECTOR, capture + k * S, &Payload::Marker(Marker { taxonomy: 4, polarity, text: note.into(), attachment: None }));
|
||||
}
|
||||
rm.emit(DIRECTOR, s.stop_t, &Payload::Control { target: pk(DEVICE), command: Command::RecordStop });
|
||||
let chat = rm.emit(VIEWER, s.start_t + 500 * MS, &Payload::Chat { text: "good luck".into() });
|
||||
let chat = rm.emit(VIEWER, s.start_t + 500 * MS, &Payload::Chat { text: "good luck".into(), origin: None });
|
||||
// The chunks, as captured, as the device commits them, and as the manifest lists them.
|
||||
let device_time = |t: u64| (t as i128 - s.offset as i128) as u64;
|
||||
let bytes: Vec<Vec<u8>> = (0..s.chunks.len()).map(|i| chunk_bytes(s.label.as_bytes(), i)).collect();
|
||||
|
|
|
|||
|
|
@ -223,6 +223,7 @@ pub const CAP_LIVE_WATERMARK: &str = "wai.live.watermark";
|
|||
pub const CAP_LIVE_ADS: &str = "wai.live.ads";
|
||||
pub const CAP_LIVE_PROVENANCE: &str = "wai.live.provenance";
|
||||
pub const CAP_EDGE_DERIVATION: &str = "wai.edge.derivation";
|
||||
pub const CAP_MEDIA_SYNTHESIS: &str = "wai.media.synthesis";
|
||||
|
||||
/// One row per string SPEC §5 registers that is a payload, companion or record:
|
||||
/// `(string, SPEC section, edition, registered)`.
|
||||
|
|
@ -314,6 +315,7 @@ const ROWS: &[(&str, &str, Option<&str>, &str)] = &[
|
|||
(CAP_LIVE_WATERMARK, S_INTERACTIVE, None, "2026-10-20"),
|
||||
(CAP_LIVE_ADS, S_INTERACTIVE, None, "2026-10-20"),
|
||||
(CAP_LIVE_PROVENANCE, S_INTERACTIVE, None, "2026-10-21"),
|
||||
(CAP_MEDIA_SYNTHESIS, 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"),
|
||||
|
|
@ -368,6 +370,7 @@ const RECORDS: &[&str] = &[
|
|||
CAP_LIVE_ADS,
|
||||
CAP_LIVE_PROVENANCE,
|
||||
CAP_EDGE_DERIVATION,
|
||||
CAP_MEDIA_SYNTHESIS,
|
||||
];
|
||||
|
||||
/// A named identifier SPEC uses that has the shape of a capability string but is
|
||||
|
|
@ -526,6 +529,7 @@ const FORENSIC_WATERMARK: &str = "extensions/forensic-watermark.md";
|
|||
const LIVE_ADS: &str = "extensions/live-ads.md";
|
||||
const LIVE_PROVENANCE: &str = "extensions/live-provenance.md";
|
||||
const DERIVING_EDGE: &str = "extensions/deriving-edge.md";
|
||||
const SYNTHESIS: &str = "extensions/synthesis.md";
|
||||
|
||||
const fn wire(name: &'static str, kind: WireKind, document: &'static str, section: &'static str,
|
||||
status: WireStatus) -> WireIdentifier {
|
||||
|
|
@ -536,9 +540,9 @@ 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, of live ads and of
|
||||
/// live provenance and of the deriving edge, in document order. The export lists them sorted by
|
||||
/// name and kind.
|
||||
/// of live moderation, of confidentiality, of forensic watermark sessions, of live ads, of live
|
||||
/// provenance, of the deriving edge and of synthesis, 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),
|
||||
|
|
@ -627,6 +631,12 @@ pub const WIRE_IDENTIFIERS: &[WireIdentifier] = &[
|
|||
wire("wai_c2pa_track", WireKind::Catalog, LIVE_PROVENANCE, "§10.2", WireStatus::Defined),
|
||||
wire("wai-c2pa", WireKind::Catalog, LIVE_PROVENANCE, "§10.2", WireStatus::Defined),
|
||||
wire("wai-derivation", WireKind::DocumentKind, DERIVING_EDGE, "§5.1", WireStatus::Defined),
|
||||
wire("wai-origin", WireKind::DocumentKind, SYNTHESIS, "§3.1", WireStatus::Defined),
|
||||
wire("wai-marking", WireKind::DocumentKind, SYNTHESIS, "§7.2", WireStatus::Defined),
|
||||
wire("wai-detection", WireKind::DocumentKind, SYNTHESIS, "§7.3", WireStatus::Defined),
|
||||
wire("wai-review", WireKind::DocumentKind, SYNTHESIS, "§9.1", WireStatus::Defined),
|
||||
wire("wai-label-policy", WireKind::DocumentKind, SYNTHESIS, "§10.1", WireStatus::Defined),
|
||||
wire("wai-player", WireKind::DocumentKind, SYNTHESIS, "§10.3", WireStatus::Defined),
|
||||
];
|
||||
|
||||
/// A string with the shape of a capability that the reference implementation uses
|
||||
|
|
|
|||
|
|
@ -460,6 +460,15 @@ pub mod energy_steering;
|
|||
/// cbindgen:ignore
|
||||
#[cfg(feature = "live_moderation")]
|
||||
pub mod live_moderation;
|
||||
// Synthesis provenance (extensions/synthesis.md): `wai.media.synthesis` — the origin claim of an
|
||||
// object, a group or a track and its class (captured, reconstructed, assistive, synthesized),
|
||||
// its IPTC digital source type and C2PA statement, assistive claims proved by recomputing a pinned
|
||||
// integer derivation, the two marking layers and their agreement, AI participants' tracks,
|
||||
// reviews presented only on a decode path of their tier, and a player's label log. Rust-only,
|
||||
// like the live modules: its constants must not become macros in include/wai.h.
|
||||
/// cbindgen:ignore
|
||||
#[cfg(feature = "synthesis")]
|
||||
pub mod synthesis;
|
||||
// Forensic watermark sessions (extensions/forensic-watermark.md): `wai.live.watermark` — the edge's
|
||||
// session record of the ETSI TS 104 002 A/B variants it embedded, bound to the session's grant by a
|
||||
// salted commitment, the recovered sequence and the rule that matches it, and the room authority's
|
||||
|
|
|
|||
|
|
@ -409,7 +409,7 @@ struct LiveBreak {
|
|||
|
||||
fn live_break(spec: Spec) -> LiveBreak {
|
||||
let mut room = room(spec);
|
||||
room.raw(&key(PRODUCER), 10 * S, kind::CHAT, Payload::Chat { text: "kick-off".into() }.encode());
|
||||
room.raw(&key(PRODUCER), 10 * S, kind::CHAT, Payload::Chat { text: "kick-off".into(), origin: None }.encode());
|
||||
let out = room.cue(PRODUCER, 60 * S, cue_type::OUT, 101, 0);
|
||||
let in_ = room.cue(PRODUCER, 90 * S, cue_type::IN, 101, 30_000);
|
||||
let policy = policy(&room, |_| {});
|
||||
|
|
|
|||
|
|
@ -826,10 +826,10 @@ fn studio(v: Variant) -> Written {
|
|||
c.life(S, DIRECTOR, own(media::VIDEO, 2, 2));
|
||||
c.life(2 * S, MIXER, own(media::VIDEO, 3, 3));
|
||||
c.life(2 * S, MIXER, own(media::AUDIO, 1, 1));
|
||||
c.emit(VIEWER, 2_500 * MS, &Payload::Chat { text: "hello".into() });
|
||||
c.emit(VIEWER, 2_500 * MS, &Payload::Chat { text: "hello".into(), origin: None });
|
||||
c.life(3 * S, GUEST, Detail::Join { grant: gg.digest() });
|
||||
c.life(3 * S, GUEST, own(media::AUDIO, 0, 1));
|
||||
c.emit(VIEWER, 3_500 * MS, &Payload::Chat { text: "welcome".into() });
|
||||
c.emit(VIEWER, 3_500 * MS, &Payload::Chat { text: "welcome".into(), origin: None });
|
||||
c.life(4 * S, SCREEN, Detail::Join { grant: gs.digest() });
|
||||
c.life(4 * S, SCREEN, own(media::SCREEN, 4, 1));
|
||||
c.life(4 * S, ENCODER, Detail::Join { grant: ge.digest() });
|
||||
|
|
|
|||
|
|
@ -446,6 +446,30 @@ pub mod scte_form {
|
|||
pub const EVENT_BASED: u8 = 2;
|
||||
}
|
||||
|
||||
/// The origin a `chat` or a `reaction` may state (§4.4, §4.5), as a `caption` states its
|
||||
/// own (§4.10).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Origin {
|
||||
/// 1 human, 2 machine, 3 machine-reviewed.
|
||||
pub origin: u8,
|
||||
/// The digest naming the model, present exactly when `origin` is 2 or 3.
|
||||
pub model: Option<[u8; 32]>,
|
||||
}
|
||||
|
||||
impl Origin {
|
||||
/// Whether it states that a machine produced the content: origin 2 or 3.
|
||||
pub fn is_machine(&self) -> bool {
|
||||
matches!(self.origin, 2 | 3)
|
||||
}
|
||||
|
||||
fn encode(&self, o: &mut Vec<u8>) {
|
||||
o.push(self.origin);
|
||||
if let Some(m) = self.model {
|
||||
o.extend_from_slice(&m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A `reaction` (§4.5).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Reaction {
|
||||
|
|
@ -453,6 +477,8 @@ pub struct Reaction {
|
|||
pub value: u16,
|
||||
pub target: Option<[u8; 32]>,
|
||||
pub remove: bool,
|
||||
/// The origin it states, if it states one.
|
||||
pub origin: Option<Origin>,
|
||||
}
|
||||
|
||||
/// A `marker` (§4.8).
|
||||
|
|
@ -492,7 +518,8 @@ pub struct Caption {
|
|||
pub enum Payload {
|
||||
Meta(Vec<u8>),
|
||||
Cue(Cue),
|
||||
Chat { text: String },
|
||||
/// A message, and the origin it states, if it states one (§4.4).
|
||||
Chat { text: String, origin: Option<Origin> },
|
||||
Reaction(Reaction),
|
||||
PollOpen { question: String, options: Vec<String> },
|
||||
Vote { poll: [u8; 32], option: u8 },
|
||||
|
|
@ -549,7 +576,12 @@ impl Payload {
|
|||
}
|
||||
}
|
||||
}
|
||||
Payload::Chat { text } => put_text(&mut o, text),
|
||||
Payload::Chat { text, origin } => {
|
||||
put_text(&mut o, text);
|
||||
if let Some(g) = origin {
|
||||
g.encode(&mut o);
|
||||
}
|
||||
}
|
||||
Payload::Reaction(r) => {
|
||||
o.push(r.rtype);
|
||||
o.extend_from_slice(&r.value.to_le_bytes());
|
||||
|
|
@ -557,6 +589,9 @@ impl Payload {
|
|||
if let Some(t) = r.target {
|
||||
o.extend_from_slice(&t);
|
||||
}
|
||||
if let Some(g) = &r.origin {
|
||||
g.encode(&mut o);
|
||||
}
|
||||
}
|
||||
Payload::PollOpen { question, options } => {
|
||||
put_text(&mut o, question);
|
||||
|
|
@ -670,6 +705,22 @@ pub fn splice_info_section_ok(b: &[u8]) -> bool {
|
|||
b.len() >= 3 && b[0] == 0xFC && 3 + ((((b[1] & 0x0F) as usize) << 8) | b[2] as usize) == b.len()
|
||||
}
|
||||
|
||||
/// §4.4, §4.5: the optional origin after a `chat`'s or a `reaction`'s last field — `origin`
|
||||
/// (1, 2 or 3), then `model` for 2 and 3 — or none when no byte follows.
|
||||
fn read_origin(r: &mut Rd) -> Result<Option<Origin>, Reason> {
|
||||
if r.done() {
|
||||
return Ok(None);
|
||||
}
|
||||
let m = Reason::MalformedPayload;
|
||||
let origin = r.u8().ok_or(m)?;
|
||||
let model = match origin {
|
||||
1 => None,
|
||||
2 | 3 => Some(r.d32().ok_or(m)?),
|
||||
_ => return Err(m),
|
||||
};
|
||||
Ok(Some(Origin { origin, model }))
|
||||
}
|
||||
|
||||
/// Read a payload of kind `k` (§4), in the order its fields are written; each field's own
|
||||
/// rule applies when it is read, and bytes after the last field are `malformed_payload`.
|
||||
/// `k` is a kind this revision defines.
|
||||
|
|
@ -701,7 +752,10 @@ pub fn parse_payload(k: u8, b: &[u8], max_text_cp: u16) -> Result<Payload, Reaso
|
|||
};
|
||||
Payload::Cue(Cue { cue_type: ct, cue_id, duration_ms, scte })
|
||||
}
|
||||
kind::CHAT => Payload::Chat { text: read_text(&mut r, max_text_cp)? },
|
||||
kind::CHAT => {
|
||||
let text = read_text(&mut r, max_text_cp)?;
|
||||
Payload::Chat { text, origin: read_origin(&mut r)? }
|
||||
}
|
||||
kind::REACTION => {
|
||||
let rtype = r.u8().ok_or(m)?;
|
||||
if !(1..=5).contains(&rtype) {
|
||||
|
|
@ -716,7 +770,7 @@ pub fn parse_payload(k: u8, b: &[u8], max_text_cp: u16) -> Result<Payload, Reaso
|
|||
return Err(m);
|
||||
}
|
||||
let target = if flags & 1 != 0 { Some(r.d32().ok_or(m)?) } else { None };
|
||||
Payload::Reaction(Reaction { rtype, value, target, remove: flags & 2 != 0 })
|
||||
Payload::Reaction(Reaction { rtype, value, target, remove: flags & 2 != 0, origin: read_origin(&mut r)? })
|
||||
}
|
||||
kind::POLL_OPEN => {
|
||||
let question = read_text(&mut r, max_text_cp)?;
|
||||
|
|
@ -1692,7 +1746,7 @@ mod tests {
|
|||
}
|
||||
|
||||
fn chat(s: &str) -> Payload {
|
||||
Payload::Chat { text: s.into() }
|
||||
Payload::Chat { text: s.into(), origin: None }
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1746,7 +1800,11 @@ mod tests {
|
|||
Payload::Cue(Cue { cue_type: cue_type::BREAK, cue_id: 8, duration_ms: 0, scte: Some((2, vec![1])) }),
|
||||
Payload::Cue(Cue { cue_type: cue_type::CHAPTER, cue_id: 9, duration_ms: 1, scte: None }),
|
||||
chat("héllo"),
|
||||
Payload::Reaction(Reaction { rtype: 1, value: 3, target: Some(d), remove: true }),
|
||||
Payload::Chat { text: "hi".into(), origin: Some(Origin { origin: 1, model: None }) },
|
||||
Payload::Chat { text: "hi".into(), origin: Some(Origin { origin: 2, model: Some(d) }) },
|
||||
Payload::Reaction(Reaction { rtype: 1, value: 3, target: Some(d), remove: true, origin: None }),
|
||||
Payload::Reaction(Reaction { rtype: 1, value: 3, target: Some(d), remove: false, origin: Some(Origin { origin: 3, model: Some(d) }) }),
|
||||
Payload::Reaction(Reaction { rtype: 4, value: 1, target: None, remove: false, origin: Some(Origin { origin: 2, model: Some(d) }) }),
|
||||
Payload::PollOpen { question: "q".into(), options: vec!["a".into(), "b".into()] },
|
||||
Payload::Vote { poll: d, option: 1 },
|
||||
Payload::PollClose { poll: d, counts: vec![1, 2] },
|
||||
|
|
@ -1772,6 +1830,22 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// §4.4, §4.5: a chat or a reaction states an origin of 1, 2 or 3, with a model exactly
|
||||
/// for 2 and 3; anything else after its last field is `malformed_payload`.
|
||||
#[test]
|
||||
fn a_stated_origin_reads_as_a_captions_does() {
|
||||
let text = chat("x").encode();
|
||||
for (tail, ok) in [(vec![1], true), (vec![2], false), (vec![1, 0], false), (vec![0], false), (vec![4], false)] {
|
||||
let b = [text.clone(), tail.clone()].concat();
|
||||
assert_eq!(parse_payload(kind::CHAT, &b, 500).is_ok(), ok, "{tail:?}");
|
||||
}
|
||||
let b = [text.clone(), vec![3], vec![5; 32]].concat();
|
||||
assert_eq!(parse_payload(kind::CHAT, &b, 500), Ok(Payload::Chat { text: "x".into(), origin: Some(Origin { origin: 3, model: Some([5; 32]) }) }));
|
||||
let r = Payload::Reaction(Reaction { rtype: 4, value: 1, target: None, remove: false, origin: None }).encode();
|
||||
assert_eq!(parse_payload(kind::REACTION, &[r.clone(), vec![2]].concat(), 500), Err(Reason::MalformedPayload));
|
||||
assert!(parse_payload(kind::REACTION, &[r, vec![2], vec![6; 32]].concat(), 500).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_rules() {
|
||||
let mut b = vec![2, 0, 0xC0, 0xAF];
|
||||
|
|
@ -1840,7 +1914,7 @@ mod tests {
|
|||
let mut a = Author::new(4);
|
||||
let rs: Vec<Vec<u8>> = [0, 500_000, 1_000_000].iter().map(|&t| a.emit(t, chat("x")).to_bytes()).collect();
|
||||
assert_eq!(verify_chain(ROOM, p, &rs), Outcome::Accept);
|
||||
let r = |target| Payload::Reaction(Reaction { rtype: 1, value: 1, target, remove: false });
|
||||
let r = |target| Payload::Reaction(Reaction { rtype: 1, value: 1, target, remove: false, origin: None });
|
||||
let mut a = Author::new(4);
|
||||
let rs = vec![a.emit(0, r(None)).to_bytes(), a.emit(10, r(Some([1; 32]))).to_bytes(), a.emit(999_999, r(None)).to_bytes()];
|
||||
assert_eq!(verify_chain(ROOM, Policy::default(), &rs), Outcome::Refuse { index: 2, reason: Reason::RateExceeded });
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ fn video(group: u64, object: u64) -> Anchor {
|
|||
}
|
||||
|
||||
fn chat(s: &str) -> Payload {
|
||||
Payload::Chat { text: s.into() }
|
||||
Payload::Chat { text: s.into(), origin: None }
|
||||
}
|
||||
|
||||
fn digest_of(record: &[u8]) -> [u8; 32] {
|
||||
|
|
@ -165,7 +165,7 @@ fn caption(duration_ms: u32, origin: u8, lang: &str, text: &str) -> Payload {
|
|||
}
|
||||
|
||||
fn reaction(rtype: u8, value: u16, target: Option<[u8; 32]>, remove: bool) -> Payload {
|
||||
Payload::Reaction(Reaction { rtype, value, target, remove })
|
||||
Payload::Reaction(Reaction { rtype, value, target, remove, origin: None })
|
||||
}
|
||||
|
||||
fn lifecycle(kind: u8, subject: u8, detail: &[u8]) -> Payload {
|
||||
|
|
@ -333,6 +333,22 @@ fn good_vectors() -> Vec<Vector> {
|
|||
Intent::Accept,
|
||||
));
|
||||
|
||||
let mut a = Author::new(ALICE);
|
||||
let model = sha(&[b"wai:live-events/corpus/model/", b"chat"]);
|
||||
let said = a.at(S, Payload::Chat { text: "Here is a summary of the talk so far.".into(), origin: Some(Origin { origin: 2, model: Some(model) }) });
|
||||
let target = digest_of(&said);
|
||||
out.push(v(
|
||||
"kind_chat_origin",
|
||||
"chat and reaction stating their origin (§4.4, §4.5): a machine message with its model's digest, a machine-reviewed one, a human one stating origin 1, and a machine reaction targeting the first",
|
||||
vec![
|
||||
said,
|
||||
a.at(2 * S, Payload::Chat { text: "Reviewed: the summary above is accurate.".into(), origin: Some(Origin { origin: 3, model: Some(model) }) }),
|
||||
a.at(3 * S, Payload::Chat { text: "Typed by a person.".into(), origin: Some(Origin { origin: 1, model: None }) }),
|
||||
a.at(4 * S, Payload::Reaction(Reaction { rtype: 1, value: 1, target: Some(target), remove: false, origin: Some(Origin { origin: 2, model: Some(model) }) })),
|
||||
],
|
||||
Intent::Accept,
|
||||
));
|
||||
|
||||
let mut h = Author::new(HOST);
|
||||
out.push(v(
|
||||
"kind_caption",
|
||||
|
|
@ -653,6 +669,15 @@ fn negative_vectors() -> Vec<Vector> {
|
|||
));
|
||||
let mut h = Author::new(HOST);
|
||||
out.push(v("malformed_payload_lang", "a caption whose language tag is one letter (§4.10)", vec![h.at(S, caption(1000, 1, "e", "x"))], refuse(0, Reason::MalformedPayload)));
|
||||
let mut a = Author::new(ALICE);
|
||||
let mut bytes = chat("which model?").encode();
|
||||
bytes.push(2);
|
||||
out.push(v(
|
||||
"malformed_payload_origin",
|
||||
"a chat stating origin 2 with no model digest after it (§4.4)",
|
||||
vec![a.raw(S, None, kind::CHAT, bytes).to_bytes()],
|
||||
refuse(0, Reason::MalformedPayload),
|
||||
));
|
||||
let mut h = Author::new(HOST);
|
||||
out.push(v("malformed_payload_poll", "a poll_open with one option (§4.6)", vec![h.at(S, poll_open("Only one?", &["Yes"]))], refuse(0, Reason::MalformedPayload)));
|
||||
|
||||
|
|
|
|||
|
|
@ -376,8 +376,8 @@ fn lesson(s: Scene) -> WrittenRoom {
|
|||
for p in &plans {
|
||||
c.emit(DIRECTOR, STOP_T, &Payload::Control { target: pk(p.device), command: Command::RecordStop });
|
||||
}
|
||||
c.emit(VIEWER, START_T + 500 * MS, &Payload::Chat { text: "good luck".into() });
|
||||
c.emit(VIEWER2, 18 * S, &Payload::Chat { text: "bravo".into() });
|
||||
c.emit(VIEWER, START_T + 500 * MS, &Payload::Chat { text: "good luck".into(), origin: None });
|
||||
c.emit(VIEWER2, 18 * S, &Payload::Chat { text: "bravo".into(), origin: None });
|
||||
// Each take: its commitments in the device's chain, and its manifest.
|
||||
let mut takes = Vec::new();
|
||||
for (k, p) in plans.iter().enumerate() {
|
||||
|
|
|
|||
|
|
@ -179,11 +179,11 @@ fn role(name: &str, powers: u8, events: &[u8]) -> Role {
|
|||
}
|
||||
|
||||
fn chat(s: &str) -> Payload {
|
||||
Payload::Chat { text: s.into() }
|
||||
Payload::Chat { text: s.into(), origin: None }
|
||||
}
|
||||
|
||||
fn emoji(target: Option<[u8; 32]>) -> Payload {
|
||||
Payload::Reaction(Reaction { rtype: 1, value: 1, target, remove: false })
|
||||
Payload::Reaction(Reaction { rtype: 1, value: 1, target, remove: false, origin: None })
|
||||
}
|
||||
|
||||
/// The chat room: a host and a moderator with the moderate power, two viewers, and the
|
||||
|
|
|
|||
|
|
@ -344,7 +344,7 @@ fn write(s: &Scene) -> Written {
|
|||
for t in &s.takes {
|
||||
rm.emit(DIRECTOR, s.stop_t, &Payload::Control { target: pk(t.device), command: Command::RecordStop });
|
||||
}
|
||||
rm.emit(VIEWER, s.start_t + 500 * MS, &Payload::Chat { text: "good luck".into() });
|
||||
rm.emit(VIEWER, s.start_t + 500 * MS, &Payload::Chat { text: "good luck".into(), origin: None });
|
||||
// Each take.
|
||||
let mut files = BTreeMap::new();
|
||||
let mut written = Vec::new();
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ pub enum Reason {
|
|||
ParticipantLimit,
|
||||
OutsideGrantWindow,
|
||||
PowerNotGranted,
|
||||
AiUnmarked,
|
||||
PublishNotPermitted,
|
||||
ProfileNotGranted,
|
||||
AlreadyPublished,
|
||||
|
|
@ -179,7 +180,7 @@ impl From<events::Reason> for Reason {
|
|||
|
||||
impl Reason {
|
||||
/// The codes this extension adds, in §11's order.
|
||||
pub const ALL: [Reason; 42] = [
|
||||
pub const ALL: [Reason; 43] = [
|
||||
Reason::DescriptorInvalid,
|
||||
Reason::KeyLogMissing,
|
||||
Reason::KeyLogNotHeld,
|
||||
|
|
@ -201,6 +202,7 @@ impl Reason {
|
|||
Reason::ParticipantLimit,
|
||||
Reason::OutsideGrantWindow,
|
||||
Reason::PowerNotGranted,
|
||||
Reason::AiUnmarked,
|
||||
Reason::PublishNotPermitted,
|
||||
Reason::ProfileNotGranted,
|
||||
Reason::AlreadyPublished,
|
||||
|
|
@ -249,6 +251,7 @@ impl Reason {
|
|||
Reason::ParticipantLimit => "participant_limit",
|
||||
Reason::OutsideGrantWindow => "outside_grant_window",
|
||||
Reason::PowerNotGranted => "power_not_granted",
|
||||
Reason::AiUnmarked => "ai_unmarked",
|
||||
Reason::PublishNotPermitted => "publish_not_permitted",
|
||||
Reason::ProfileNotGranted => "profile_not_granted",
|
||||
Reason::AlreadyPublished => "already_published",
|
||||
|
|
@ -1203,8 +1206,11 @@ impl Grant {
|
|||
if raw.origins.len() > 8 || !raw.origins.iter().all(|o| token_ok(o, 255)) {
|
||||
return bad;
|
||||
}
|
||||
if read_attrs(&raw.attrs).is_none() {
|
||||
return bad;
|
||||
match read_attrs(&raw.attrs) {
|
||||
None => return bad,
|
||||
// §4.4: `ai` is a model's digest, as 64 lowercase hexadecimal digits.
|
||||
Some(a) if a.iter().any(|(k, v)| k == AI_ATTR && hex32(v).is_none()) => return bad,
|
||||
Some(_) => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1237,6 +1243,12 @@ impl Grant {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// The model an AI participant's grant names in its `ai` attribute (§4.4), or `None` for
|
||||
/// a grant without one.
|
||||
pub fn ai_model(&self) -> Option<[u8; 32]> {
|
||||
self.attrs.iter().find(|(k, _)| k == AI_ATTR).and_then(|(_, v)| hex32(v))
|
||||
}
|
||||
|
||||
/// The revocation tag of the grant's viewer (§5), when it names one.
|
||||
pub fn viewer_tag(&self) -> Option<[u8; 32]> {
|
||||
(self.viewer != [0; 16]).then(|| sha256(&[VIEWER_DOMAIN, &self.viewer]))
|
||||
|
|
@ -1268,6 +1280,36 @@ fn read_grant(b: &[u8], d: &Descriptor) -> Result<(RawGrant, [u8; 32]), Reason>
|
|||
Ok((raw, sha256(&[body])))
|
||||
}
|
||||
|
||||
/// The attribute that marks an AI participant (§4.4).
|
||||
pub const AI_ATTR: &str = "ai";
|
||||
|
||||
/// 64 lowercase hexadecimal digits, read as 32 bytes.
|
||||
fn hex32(s: &str) -> Option<[u8; 32]> {
|
||||
let b = s.as_bytes();
|
||||
if b.len() != 64 || !b.iter().all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(c)) {
|
||||
return None;
|
||||
}
|
||||
let mut o = [0u8; 32];
|
||||
for (i, o) in o.iter_mut().enumerate() {
|
||||
*o = u8::from_str_radix(&s[2 * i..2 * i + 2], 16).ok()?;
|
||||
}
|
||||
Some(o)
|
||||
}
|
||||
|
||||
/// §9.2 step 6: an AI participant's `chat`, `reaction` and `caption` state that a machine
|
||||
/// produced them, with the model its grant names. Any other payload, or a principal whose
|
||||
/// grant names no model, is not held to it.
|
||||
pub fn ai_marked(g: &Grant, p: &Payload) -> bool {
|
||||
let Some(model) = g.ai_model() else { return true };
|
||||
let stated = match p {
|
||||
Payload::Chat { origin, .. } => origin.map(|o| (o.origin, o.model)),
|
||||
Payload::Reaction(r) => r.origin.map(|o| (o.origin, o.model)),
|
||||
Payload::Caption(c) => Some((c.origin, c.model)),
|
||||
_ => return true,
|
||||
};
|
||||
matches!(stated, Some((2 | 3, Some(m))) if m == model)
|
||||
}
|
||||
|
||||
/// A grant a room verifier holds, with its digest.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct HeldGrant {
|
||||
|
|
@ -1988,6 +2030,12 @@ fn step(replay: &mut Replay, verifier: &mut RoomVerifier, seen: &mut Seen, b: &[
|
|||
return Err(Reason::PowerNotGranted);
|
||||
}
|
||||
}
|
||||
// f. An AI participant's chat, reaction and caption state their machine origin (§4.4).
|
||||
if let Some(a) = verifier.accepted().last()
|
||||
&& !ai_marked(&h.grant, &a.payload)
|
||||
{
|
||||
return Err(Reason::AiUnmarked);
|
||||
}
|
||||
Ok(Some(h.digest))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@ impl Build {
|
|||
}
|
||||
|
||||
fn chat(s: &str) -> Payload {
|
||||
Payload::Chat { text: s.into() }
|
||||
Payload::Chat { text: s.into(), origin: None }
|
||||
}
|
||||
|
||||
fn control(target: u8, command: Command) -> Payload {
|
||||
|
|
@ -372,7 +372,7 @@ fn lesson() -> (Build, [u8; 32]) {
|
|||
b.ev(PERFORMER, 8 * S, take_commit(1));
|
||||
b.ev(DIRECTOR, 9 * S, Payload::Marker(Marker { taxonomy: 2, polarity: 2, text: "rushed bar 12".into(), attachment: None }));
|
||||
b.ev(DIRECTOR, 10 * S, control(PERFORMER, Command::RecordStop));
|
||||
b.ev(VIEWER_B, 10_500_000, Payload::Reaction(Reaction { rtype: 4, value: 1, target: None, remove: false }));
|
||||
b.ev(VIEWER_B, 10_500_000, Payload::Reaction(Reaction { rtype: 4, value: 1, target: None, remove: false, origin: None }));
|
||||
b.ev(DIRECTOR, 11 * S, chat("again from bar 9"));
|
||||
b.life(12 * S, VIEWER_B, Detail::Leave { reason: leave::LEFT });
|
||||
(b, revoked)
|
||||
|
|
@ -408,7 +408,7 @@ fn broadcast() -> Build {
|
|||
b.ev(VIEWER_C, 4_200_000, Payload::Vote { poll, option: 0 });
|
||||
let spam = b.ev(VIEWER_C, 5 * S, chat("first!!! follow the link in my name"));
|
||||
b.ev(MODERATOR, 5_500_000, Payload::Decision { verdict: 2, target: spam, reason: 3 });
|
||||
b.ev(VIEWER_A, 6 * S, Payload::Reaction(Reaction { rtype: 4, value: 1, target: None, remove: false }));
|
||||
b.ev(VIEWER_A, 6 * S, Payload::Reaction(Reaction { rtype: 4, value: 1, target: None, remove: false, origin: None }));
|
||||
b.ev(HOST, 7 * S, Payload::PollClose { poll, counts: vec![1, 2] });
|
||||
b.life(8 * S, VIEWER_C, Detail::Leave { reason: leave::REMOVED });
|
||||
b
|
||||
|
|
@ -483,7 +483,7 @@ fn dvr() -> Build {
|
|||
b.join(S, VIEWER_A, da);
|
||||
b.join(2 * S, VIEWER_B, db);
|
||||
b.ev(VIEWER_A, 3 * S, chat("going back to the opening"));
|
||||
b.ev(VIEWER_B, 4 * S, Payload::Reaction(Reaction { rtype: 4, value: 1, target: None, remove: false }));
|
||||
b.ev(VIEWER_B, 4 * S, Payload::Reaction(Reaction { rtype: 4, value: 1, target: None, remove: false, origin: None }));
|
||||
b
|
||||
}
|
||||
|
||||
|
|
@ -527,7 +527,7 @@ fn good_vectors() -> Vec<Vector> {
|
|||
b.publish(11 * S, VIEWER_A, "", media::AUDIO, 0, 1);
|
||||
b.publish(11 * S, VIEWER_A, "", media::VIDEO, 1, 2);
|
||||
b.ev(VIEWER_A, 12 * S, Payload::Marker(Marker { taxonomy: 1, polarity: 1, text: "question".into(), attachment: None }));
|
||||
b.ev(VIEWER_B, 12 * S, Payload::Reaction(Reaction { rtype: 4, value: 1, target: None, remove: false }));
|
||||
b.ev(VIEWER_B, 12 * S, Payload::Reaction(Reaction { rtype: 4, value: 1, target: None, remove: false, origin: None }));
|
||||
out.push(vector(
|
||||
"promote_viewer",
|
||||
"a viewer's audience grant exchanged at the 10 s tick for a guest grant chained to it: before, it may chat only; after, it publishes audio and video and sends a marker (§4.6)",
|
||||
|
|
@ -711,6 +711,12 @@ fn grant_vectors() -> Vec<Vector> {
|
|||
&one(&|b| Grant { policy: Policy::with_kinds(&[kind::CHAT, kind::JOIN]), ..b.grant(VIEWER_A, 2) }.sign(&key(AUTHORITY)).to_bytes()),
|
||||
refuse(1, Reason::GrantInvalid),
|
||||
));
|
||||
out.push(vector(
|
||||
"grant_ai_malformed",
|
||||
"a grant whose ai attribute is not 64 lowercase hexadecimal digits (§4.4)",
|
||||
&one(&|b| Grant { attrs: vec![("ai".into(), "A".repeat(64))], ..b.grant(VIEWER_A, 2) }.sign(&key(AUTHORITY)).to_bytes()),
|
||||
refuse(1, Reason::GrantInvalid),
|
||||
));
|
||||
out.push(vector(
|
||||
"grant_attrs_too_long",
|
||||
"a grant whose attributes take more than 1024 bytes (§4.4)",
|
||||
|
|
@ -1008,6 +1014,38 @@ fn record_vectors() -> Vec<Vector> {
|
|||
let r = refuse_last(&b, HOST, Reason::PowerNotGranted);
|
||||
out.push(vector("power_not_granted", "a host with the direct power but not record mutes a viewer, then sends record_start (§3.3)", &b, r));
|
||||
|
||||
// An AI participant (§4.4): a host whose grant names the model that acts for it.
|
||||
let model = sha(&[b"wai:live-rooms/corpus/model"]);
|
||||
let ai_room = |says: Option<events::Origin>| {
|
||||
let mut b = Build::new(base());
|
||||
let g = Grant { attrs: vec![("ai".into(), hex(&model))], ..b.grant(HOST, 0) };
|
||||
let gv = b.grant(VIEWER_A, 2);
|
||||
let (d, dv) = (b.add(g), b.add(gv));
|
||||
b.join(0, HOST, d);
|
||||
b.join(0, VIEWER_A, dv);
|
||||
b.ev(VIEWER_A, S, Payload::Chat { text: "Is anyone there?".into(), origin: None });
|
||||
b.ev(HOST, 2 * S, Payload::Chat { text: "Welcome. I am an automated host.".into(), origin: says });
|
||||
b
|
||||
};
|
||||
let mut b = ai_room(Some(events::Origin { origin: 2, model: Some(model) }));
|
||||
b.ev(HOST, 3 * S, Payload::Caption(events::Caption { duration_ms: 2_000, origin: 3, model: Some(model), lang: "en".into(), text: "Welcome.".into() }));
|
||||
b.ev(VIEWER_A, 4 * S, Payload::Reaction(events::Reaction { rtype: 4, value: 1, target: None, remove: false, origin: None }));
|
||||
out.push(vector(
|
||||
"ai_participant",
|
||||
"a host whose grant's ai attribute names a model: its chat states origin 2 and its caption origin 3, each with that model; a person's chat and reaction state none (§4.4, §9.2 step 6)",
|
||||
&b,
|
||||
Intent::Accept,
|
||||
));
|
||||
let b = ai_room(None);
|
||||
let r = refuse_last(&b, HOST, Reason::AiUnmarked);
|
||||
out.push(vector("ai_unmarked", "an AI participant's chat that states no origin (§9.2 step 6)", &b, r));
|
||||
let b = ai_room(Some(events::Origin { origin: 1, model: None }));
|
||||
let r = refuse_last(&b, HOST, Reason::AiUnmarked);
|
||||
out.push(vector("ai_unmarked_human", "an AI participant's chat that states origin 1, human (§9.2 step 6)", &b, r));
|
||||
let b = ai_room(Some(events::Origin { origin: 2, model: Some(sha(&[b"wai:live-rooms/corpus/other-model"])) }));
|
||||
let r = refuse_last(&b, HOST, Reason::AiUnmarked);
|
||||
out.push(vector("ai_unmarked_model", "an AI participant's chat that states origin 2 with a model other than its grant's (§9.2 step 6)", &b, r));
|
||||
|
||||
let mut b = Build::new(base());
|
||||
let g = b.grant(VIEWER_A, 2);
|
||||
let g = Grant { policy: Policy { max_events: 2, ..g.policy }, ..g };
|
||||
|
|
|
|||
1403
wai/wai-rs/src/synthesis.rs
Normal file
1403
wai/wai-rs/src/synthesis.rs
Normal file
File diff suppressed because it is too large
Load diff
1092
wai/wai-rs/src/synthesis/corpus.rs
Normal file
1092
wai/wai-rs/src/synthesis/corpus.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -39,7 +39,7 @@ fn the_corpus_covers_every_reason_and_room() {
|
|||
for r in Reason::ALL {
|
||||
assert!(reasons.contains(r.code()), "nothing in the corpus is refused with {r}");
|
||||
}
|
||||
for name in ["lesson", "broadcast", "takeover", "redundant_failover", "promote_viewer", "dvr"] {
|
||||
for name in ["lesson", "broadcast", "takeover", "redundant_failover", "promote_viewer", "dvr", "ai_participant"] {
|
||||
let v = vectors.iter().find(|v| v["name"] == name).unwrap_or_else(|| panic!("no vector {name}"));
|
||||
assert_eq!(v["outcome"], "accept", "{name}");
|
||||
}
|
||||
|
|
|
|||
71
wai/wai-rs/tests/synthesis.rs
Normal file
71
wai/wai-rs/tests/synthesis.rs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
//! `wai.media.synthesis` (extensions/synthesis.md): the committed corpus
|
||||
//! `live-conformance/synthesis/` 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 §12 lists
|
||||
//! exactly the reasons the reference reports. The generator itself asserts that the corpus
|
||||
//! gives every one of them.
|
||||
#![cfg(feature = "synthesis")]
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
use wai::synthesis::{corpus, Reason};
|
||||
|
||||
fn wai_dir() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..")
|
||||
}
|
||||
|
||||
fn committed() -> BTreeMap<String, String> {
|
||||
let dir = wai_dir().join("live-conformance/synthesis");
|
||||
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<String, String> = 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_synthesis_vectors gen`");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_case_gets_its_stated_outcome() {
|
||||
let fails = corpus::verify(&committed());
|
||||
assert!(fails.is_empty(), "{}", fails.join("\n"));
|
||||
}
|
||||
|
||||
/// §12 of the extension lists exactly the codes `Reason::ALL` has, each once, in order.
|
||||
#[test]
|
||||
fn the_extension_lists_the_reasons() {
|
||||
let text = std::fs::read_to_string(wai_dir().join("extensions/synthesis.md")).expect("the extension");
|
||||
let at = text.find("\n## 12. Reasons").expect("a §12");
|
||||
let end = text[at + 1..].find("\n## ").map_or(text.len(), |e| at + 1 + e);
|
||||
let listed: Vec<String> =
|
||||
text[at..end].lines().filter(|l| l.starts_with("| `")).filter_map(|l| l.split('`').nth(1).map(String::from)).collect();
|
||||
let want: Vec<String> = Reason::ALL.iter().map(|r| r.code().to_string()).collect();
|
||||
assert_eq!(listed, want, "§12's table and Reason::ALL differ");
|
||||
}
|
||||
|
||||
/// §10.2's table pins exactly the icons `EU_ICONS` holds, each by its SHA-256, in order.
|
||||
#[test]
|
||||
fn the_extension_pins_the_icons() {
|
||||
let text = std::fs::read_to_string(wai_dir().join("extensions/synthesis.md")).expect("the extension");
|
||||
let at = text.find("\n### 10.2 ").expect("a §10.2");
|
||||
let end = text[at + 1..].find("\n### ").map_or(text.len(), |e| at + 1 + e);
|
||||
let pinned: Vec<(String, String)> = text[at..end]
|
||||
.lines()
|
||||
.filter(|l| l.starts_with("| `LABEL_AI"))
|
||||
.filter_map(|l| {
|
||||
let cells: Vec<&str> = l.split('`').collect();
|
||||
Some((cells.get(1)?.to_string(), cells.get(3)?.to_string()))
|
||||
})
|
||||
.collect();
|
||||
let want: Vec<(String, String)> = wai::synthesis::EU_ICONS.iter().map(|(n, d)| (n.to_string(), d.to_string())).collect();
|
||||
assert_eq!(pinned, want, "§10.2's table and EU_ICONS differ");
|
||||
}
|
||||
Loading…
Reference in a new issue