Transparency rules for AI look like four unrelated laws until you try to ship a product under them. Then they collapse into a small number of engineering duties: tell a person they are talking to a machine, mark generated media so software can recognise it, put a visible label on certain content, and publish a summary of what the model was trained on. The EU AI Act, California's AI Transparency Act and training-data law, and China's labelling measures each pick a different subset of those duties and different modalities.
This article treats the rules as product requirements. It builds a duty-by-modality matrix as of October 2026, puts one disclosure layer in the output path that resolves and applies every duty, logs evidence that each duty fired, and walks a worked example of one assistant shipped to three jurisdictions. It is engineering guidance, not legal advice; the dates and section numbers below were checked on 6 October 2026, and two of the laws were amended during 2026, so re-check them against the primary text before you encode them. For roles and the full EU timeline, see the EU AI Act guide; for the C2PA mechanics of signing, see C2PA for AI-generated content.
Four kinds of transparency duty
Every transparency rule in force today reduces to one of four duties, and the type tells you where the code lives.
- Interaction notice. A person interacting with an AI system must know it is AI. This lives in the conversation surface: chat UI, voice agent, email agent.
- Latent marking. Generated content carries machine-readable provenance: metadata such as a C2PA manifest, an invisible watermark, or both. This lives in the generation pipeline, after the model and before the bytes leave your boundary.
- Visible labelling. Some content must carry a human-readable label: deepfakes, certain published text, and in China most synthetic content. This lives where content is rendered or exported.
- Training-data documentation. A public summary of the data behind a model. This lives in your data catalogue and release process, not in the serving path.
The first three are per-output duties enforced in code on every response; the fourth is per release.
The duty matrix as of October 2026
The table maps each regime onto the four duties. Read the cells as triggers to encode, with the primary text one click away, rather than as a summary of the law.
| Regime | Interaction notice | Latent marking | Visible label | Training data |
|---|---|---|---|---|
| EU AI Act, Art. 50 (from 2 Aug 2026) | 50(1): providers design systems that interact with people so they are informed, unless obvious from context | 50(2): providers of systems generating audio, image, video or text mark output machine-readably; grace to 2 Dec 2026 for systems already on the market | 50(4): deployers disclose deepfakes, and AI-generated text published to inform the public unless it had human editorial review and responsibility | Art. 53(1)(d): GPAI model providers publish a training-content summary on the AI Office template |
| California AI Transparency Act (operative 2 Aug 2026) | Not in this act | Latent disclosure in image, video and audio: provider, system name and version, time, unique ID, created or altered | Platforms from 1 Jan 2027 must surface provenance to users | Not in this act |
| California AB 2013 (from 1 Jan 2026) | - | - | - | Developers post documentation of training datasets for systems released since 1 Jan 2022 |
| China labelling measures (from 1 Sep 2025) | - | Implicit labels in file metadata: content attributes, provider, content ID | Explicit labels on text, audio, images, video and virtual scenes where they may mislead | - |
Three asymmetries drive most of the design. California's marking duty covers image, video and audio, not text, while the EU's covers text too. The EU splits duties between providers, who mark, and deployers, who label. And California's act was amended twice: AB 853 moved the operative date to 2 August 2026, and SB 1000 took effect on 30 September 2026. The current text calls the free checker a disclosure verification tool, sets a 72-hour clock for cutting off a licensee who strips disclosures, and defines a covered provider without the user-count threshold that older summaries quote. Earlier write-ups also describe an optional manifest (visible) disclosure; we did not find it in the current sections, so confirm before you build to it.
One disclosure layer in the output path
Put the per-output duties in one layer that every response passes through, rather than scattering them across clients. Clients forget; a mobile build from last quarter will not show the new label. The layer reads a versioned policy table, resolves the duties for this request from region, modality and surface, applies them, and writes one evidence record. The verification API, conformance tests and training-data document hang off the same policy table, so a rule change is one reviewed edit.
Encoding the policy
The policy table is data, not branching code. Each duty has a stable id that evidence records cite, so an auditor can join a log line back to a rule. Resolve over the set of regions an output can reach, not the user's billing country: a file made in Berlin and shared to a Shanghai channel needs both marks, and marking is cheap while re-marking content that already left is impossible.
from dataclasses import dataclass
@dataclass(frozen=True)
class Duty:
id: str # stable id, cited in evidence records
kind: str # "notice" | "latent" | "visible"
regions: frozenset # where the duty applies
modalities: frozenset
surfaces: frozenset = frozenset({"*"})
effective: str = "2026-08-02" # ISO date; compare as strings
POLICY = [
Duty("eu-50-1-notice", "notice", frozenset({"EU"}), frozenset({"text", "audio"}),
frozenset({"chat", "voice"})),
Duty("eu-50-2-mark", "latent", frozenset({"EU"}),
frozenset({"text", "image", "audio", "video"})),
Duty("ca-22757-3-latent", "latent", frozenset({"US-CA"}),
frozenset({"image", "audio", "video"})),
Duty("cn-implicit", "latent", frozenset({"CN"}),
frozenset({"text", "image", "audio", "video"}), effective="2025-09-01"),
Duty("cn-explicit", "visible", frozenset({"CN"}),
frozenset({"text", "image", "audio", "video"}), effective="2025-09-01"),
]
def resolve(regions, modality, surface, today):
"""Union of duties over every region the output may reach."""
return [d for d in POLICY
if d.regions & set(regions)
and modality in d.modalities
and ("*" in d.surfaces or surface in d.surfaces)
and d.effective <= today]Two things are deliberately missing. Deployer duties such as the EU deepfake label are not here when you are only the provider; give deployers a flag and a ready label instead. And the 2 December 2026 EU grace date is not encoded: if you are building now, mark from day one rather than writing logic you will delete in eight weeks.
Interaction notices
The interaction notice is the easiest duty and the one most often broken by product changes. The EU text exempts cases where AI involvement is obvious from context, but obvious to whom is a judgement; a voice agent that sounds human, or an email agent that signs with a person's name, is not obvious. Show the notice at the start of the first interaction, keep it in the persistent UI, and make the agent answer truthfully when asked whether it is a bot. Enforce that last part outside the model: a system-prompt instruction is not a control, because a persona prompt or an injection can override it. A cheap pattern is a classifier on user turns that detects the question and returns a fixed, server-side answer. For the UI side of showing users what an agent did, see transparency UX for LLM agents.
Marking generated content
Latent marking has two layers with opposite failure modes. Metadata, such as a signed C2PA manifest with the IPTC digital source type for generated media, is precise and verifiable but is stripped by screenshots, re-encoding and many upload pipelines. An invisible watermark survives some of those transformations but carries few bits and has a false-positive rate. Use both: the watermark carries a short id, and the id resolves to the full record on your side. That is how California's text allows the disclosure to convey its fields directly or through a link to a permanent website.
The California latent disclosure must convey the provider name, the system name and version, the time of creation or alteration, a unique identifier, and whether content was created or altered, and must be compatible with your verification tool. That tool must be free, accept an upload or a URL, be callable through an API, and return provenance without leaking personal information. Design the record so the id is random rather than derived from a user id; otherwise the verification tool becomes a de-anonymisation oracle.
Text is the weak spot. No marking survives a user retyping a paragraph, and statistical text watermarks degrade under paraphrase. The EU still requires text marking where technically feasible, so ship metadata where the format allows, such as API response fields and document properties, and document what you cannot mark.
Evidence that a duty fired
Regulators and enterprise customers ask the same question: show me it was applied. The layer therefore writes one record per output with the duty ids applied, the policy version, the mark id and a hash of the bytes, never the content itself.
import hashlib, json, secrets, time
def apply_duties(output, duties, ctx, sign, watermark, log):
mark_id = secrets.token_hex(12) # random: never derived from the user
applied = []
for d in duties:
if d.kind == "latent":
output.payload = watermark(output.payload, mark_id)
output.metadata["provenance"] = sign({
"provider": ctx.provider, "system": ctx.system,
"version": ctx.version, "created": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"id": mark_id, "action": output.action}) # "created" or "altered"
elif d.kind == "visible":
output.label = ctx.labels[d.id]
elif d.kind == "notice" and not ctx.session.notice_shown:
output.prefix_notice = True
applied.append(d.id)
log(json.dumps({"mark_id": mark_id, "duties": applied, "policy": ctx.policy_version,
"sha256": hashlib.sha256(output.payload).hexdigest()}))
return outputBack it with conformance tests per duty that run on every release: generate one sample per modality and region, then assert the metadata verifies, the watermark decodes, the label renders and the verification API returns the right record. A daily canary that pushes a marked image through your own upload and share flows catches the most common silent regression, a resize step that drops metadata.
Training-data documentation
Training-data documentation fails by drift, not omission. AB 2013 asks developers to post documentation covering, among other items, the sources or owners of datasets, how they further the system's purpose, rough size, whether they contain copyrighted material or personal information, whether synthetic data was used, cleaning and processing, and collection dates. The EU asks GPAI model providers for a public summary on the AI Office template. Generate both from the same data catalogue, keyed by model version, so that a retrain with a new corpus cannot ship until its summary diff has been reviewed. A hand-written web page will be wrong within two releases.
Worked example: one assistant, three jurisdictions
Take an assistant that chats and generates images, served in the EU, California and China, with an export button that saves images and an API used by a partner newsroom.
Chat in Paris. Resolve gives eu-50-1-notice and eu-50-2-mark. The first reply carries the notice; text replies carry provenance metadata in the API response. Image in San Francisco. ca-22757-3-latent fires: watermark plus signed manifest, and the verification API answers for the mark id. A text reply there gets no latent duty under California law, though the EU duty still applies if the same response may reach the EU. Image in Shanghai. cn-implicit and cn-explicit fire: metadata plus a visible label rendered into the export.
The newsroom. It is the deployer. It owns the EU deepfake label and the public-interest text disclosure unless it applies human editorial review. You ship the label strings and an API flag; it decides. The release. A retrain adds a licensed news corpus. The catalogue diff updates both training-data documents, legal reviews the diff, and the release gate checks that the documents match the model version.
A complaint about an unlabelled image is answered by the hash: find the record and read which duties applied under which policy version. A mark applied but missing from the complainant's copy means downstream stripping.
Failure modes
- Client-side labels. Old app builds never show the new label. Apply duties on the server.
- Resolving by billing country. Content travels; resolve over every region it can reach.
- Metadata stripped by your own pipeline. A thumbnailer or CDN transform removes the manifest. Canary it.
- Notice enforced by prompt. A persona or injection makes the bot claim to be human. Enforce outside the model.
- Mark ids derived from user ids. The verification tool leaks who generated what.
- Stale summaries. The training-data page describes the model two versions ago.
- Encoding secondary sources. Blog summaries of these laws went stale in 2026; encode from the primary text and cite the section.
Trade-offs
| Choice | Gain | Cost |
|---|---|---|
| Mark everything everywhere | One code path, no regional bugs | Watermark compute; marks in markets that do not need them |
| Per-region resolution | Minimal processing | Bugs when content crosses regions |
| Metadata only | Precise, verifiable | Stripped by ordinary handling |
| Watermark only | Survives some edits | Few bits, false positives, no signature |
| Shared policy table | One edit per rule change | Needs ownership and review across legal and engineering |
Most teams end up marking every generated image and audio file everywhere and resolving only visible labels per region, because visible labels change user experience while latent marks do not. Track the rules themselves with a regulatory watch and encode their applicability in a dated obligation register.
What to do next
- List every surface that emits AI output: chat, voice, API, export, email, partner feeds.
- Build the duty matrix for your regions from the primary texts and record the section for each cell.
- Put a single disclosure layer on the server path and load duties from a versioned policy table.
- Ship metadata plus watermark for image, audio and video, with random mark ids and a verification API.
- Enforce the interaction notice and the are-you-a-bot answer outside the model.
- Write one evidence record per output and a conformance test per duty per release.
- Generate training-data summaries from the data catalogue and gate releases on their diff.
- Re-read the California and EU texts each quarter; both changed in 2026.