Add EU AI Act Art. 50 compliance: disclosure and content marking - #95
Merged
Conversation
EU AI Act Art. 50 requires that a person interacting with an AI system is informed of it, unless that is already obvious. The obligation applied from 2 August 2026 and was not touched by the Digital Omnibus, which deferred the high-risk regime to December 2027. It is in force now; MATE did nothing about it. The widget is the surface the Article is really about. Embedded in someone else's page and styled to match it, nothing announces that the thing answering is an AI. So the disclosure shows there by default, as a persistent line above the input rather than part of the greeting, which is dismissed as soon as a conversation starts. There is deliberately no on/off switch. An agent has ai_disclosure for the wording and ai_disclosure_waiver for a reason not to show it, and the notice is on unless the waiver holds one. The decision to switch it off and the record of why therefore cannot come apart, because they are the same field. A reason under ten characters is refused: "n/a" is not something a reviewer could weigh. Setting or clearing a waiver writes its own audit entry rather than hiding inside a generic agent.update. Two failure choices are deliberate. If the agent row cannot be read while a widget renders, the default disclosure is shown rather than nothing — a widget that quietly stops disclosing because a query failed is the outcome the Article is written against. And the disclosure is read from the agent on every render, never stored in widget_config, because that blob is editable through the widget admin API by whoever embeds the widget, and the notice is not theirs to remove. Standalone builds always show it: there is no dashboard behind an export, so MATE_AI_DISCLOSURE translates the wording but cannot remove it. The Work Room does not, being behind a login where the person already knows what MATE is — the "already obvious" case. That reading is written down in the docs so it can be argued with. Also corrects MATE's own overclaim. audit_service described itself as "for EU AI Act compliance" and the README called the audit log "EU AI Act retention-aware". An append-only log is evidence toward Art. 12; compliance is a property of a deployed system and its operator, not of a log. A platform that implies otherwise creates liability for the people using it. 839 tests, OK. Migration V031 verified against a real database: applied, columns present, idempotent on a second run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ToCUNy2SqwvfTq4a6xfSk1
EU AI Act Art. 50(2), from 2 December 2026: synthetic content must be marked in a machine-readable form so it can be detected as artificially generated. MATE generates images and marked none of them. Generated PNGs now carry an XMP packet declaring the IPTC digital source type trainedAlgorithmicMedia — the marker the major image generators already write, so anything that reads provenance metadata reads MATE's too. The prompt goes in the description field, XML-escaped, since prompts are user text going straight into an XML document. Marking happens before the artifact is saved, so every stored copy and every URL served from it carries the same bytes. All three generation paths mark: the OpenAI-compatible path, the base64 data-URL path, and the streamed inline-data path. Marking only one of them would have been worse than marking none, because it would look done. Written directly into the PNG rather than through an imaging library. Pillow is not currently a dependency, and a PNG text chunk is less code than the dependency would be. The marker goes immediately after IHDR, which stays first as the format requires, and iTXt is an ancillary chunk so conformant decoders skip it. The risk in hand-writing chunks is a file that carries the marker and no longer decodes, so the tests do not stop at "the marker is present": they walk every chunk and verify every CRC, and check the original IHDR, IDAT and IEND survive byte for byte. On failure the original image is returned unchanged and the problem is logged. An unmarked image is a compliance gap; a corrupted one is a broken product, and the gap is the recoverable failure. This is a marker, not a signature — it states provenance, it does not prove it, and re-encoding strips it. C2PA signing is the next step but needs a signing identity MATE cannot provision for its users. Documented as such rather than left to be assumed. 850 tests, OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ToCUNy2SqwvfTq4a6xfSk1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements two obligations from EU AI Act Article 50:
Art. 50(1) — Disclosure: Users must be informed they are interacting with an AI, unless it is already obvious. MATE now shows a persistent disclosure on the widget and standalone chat surfaces. The disclosure is configurable per agent with a waiver mechanism that records why it was disabled, ensuring the decision and justification cannot be separated.
Art. 50(2) — Content Marking (effective 2 December 2026): Generated images are marked with an XMP packet carrying the IPTC digital source type
trainedAlgorithmicMedia— the same marker major image generators write. The marker is embedded before the artifact is saved, so every copy carries it.Key Changes
Disclosure (Art. 50(1))
shared/utils/ai_disclosure.pymodule withresolve_disclosure(),validate_waiver(), anddisclosure_state()helpersai_disclosure(custom text, defaults to standard notice) andai_disclosure_waiver(reason for not disclosing; minimum 10 characters to prevent silent disabling)widget_config(which is editable by the embedding site)agent.disclosure_waived/agent.disclosure_restored)Content Marking (Art. 50(2))
shared/utils/content_marking.pymodule withmark_png_as_ai_generated()andis_marked_as_ai_generated()functionsimage_tools.pybefore the artifact is saved, covering all three image generation paths (OpenAI-compatible, base64 data-URL, streamed inline)iTXtchunks with proper CRC validation; no imaging library dependencyDatabase & Audit
V031__ai_disclosure.sqladdsai_disclosureandai_disclosure_waivercolumns toagents_config(MySQL, PostgreSQL, SQLite)Tests
test_ai_disclosure.py— 11 tests covering disclosure resolution, waiver validation, and state reportingtest_content_marking.py— 13 tests validating PNG structure, CRC integrity, XMP embedding, and failure behaviortest_widget_disclosure.py— 10 tests verifying widget config serves the disclosure and that it cannot be overridden by the embedding siteImplementation Notes
widget_confighttps://claude.ai/code/session_01ToCUNy2SqwvfTq4a6xfSk1