Skip to content

Add EU AI Act Art. 50 compliance: disclosure and content marking - #95

Merged
antiv merged 2 commits into
mainfrom
claude/ai-act-disclosure
Sep 2, 2026
Merged

Add EU AI Act Art. 50 compliance: disclosure and content marking#95
antiv merged 2 commits into
mainfrom
claude/ai-act-disclosure

Conversation

@antiv

@antiv antiv commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Implements two obligations from EU AI Act Article 50:

  1. 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.

  2. 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))

  • New shared/utils/ai_disclosure.py module with resolve_disclosure(), validate_waiver(), and disclosure_state() helpers
  • Agent config gains two fields: ai_disclosure (custom text, defaults to standard notice) and ai_disclosure_waiver (reason for not disclosing; minimum 10 characters to prevent silent disabling)
  • Widget and standalone chat surfaces now show the disclosure in a persistent footer bar
  • Disclosure is read from the agent row on every render, not stored in widget_config (which is editable by the embedding site)
  • If the agent row cannot be read, the default disclosure is shown (fail toward disclosure, not silence)
  • Audit log entries track when a waiver is set or cleared (agent.disclosure_waived / agent.disclosure_restored)
  • Dashboard agent modal updated to allow configuring both fields

Content Marking (Art. 50(2))

  • New shared/utils/content_marking.py module with mark_png_as_ai_generated() and is_marked_as_ai_generated() functions
  • Marking is applied in image_tools.py before the artifact is saved, covering all three image generation paths (OpenAI-compatible, base64 data-URL, streamed inline)
  • XMP packet is written directly into PNG iTXt chunks with proper CRC validation; no imaging library dependency
  • If marking fails, the original image is returned unchanged and the failure is logged (unmarked image is a compliance gap; corrupted image is a broken product)
  • Comprehensive test suite validates PNG structure integrity, CRC correctness, idempotency, and XML escaping of user prompts

Database & Audit

  • New migration V031__ai_disclosure.sql adds ai_disclosure and ai_disclosure_waiver columns to agents_config (MySQL, PostgreSQL, SQLite)
  • Audit service updated with new action types for disclosure changes
  • Agent config snapshots and exports now include disclosure fields

Tests

  • test_ai_disclosure.py — 11 tests covering disclosure resolution, waiver validation, and state reporting
  • test_content_marking.py — 13 tests validating PNG structure, CRC integrity, XMP embedding, and failure behavior
  • test_widget_disclosure.py — 10 tests verifying widget config serves the disclosure and that it cannot be overridden by the embedding site

Implementation Notes

  • Marking is written directly into PNG rather than through an imaging library to avoid a dependency; PNG text chunks are simple enough to construct safely
  • The waiver mechanism enforces that disabling disclosure leaves a record: there is no boolean switch, only a reason field that must be at least 10 characters
  • Disclosure is read fresh on every widget render to ensure the agent row is the single source of truth, preventing the embedding site from removing the notice via widget_config
  • All three image generation paths (OpenAI, base64, streamed) mark before saving, so every stored copy and every URL served carries the marker
  • Failure modes are conservative: if anything goes wrong during marking, the original image is returned unchanged and logged, not corrupted

https://claude.ai/code/session_01ToCUNy2SqwvfTq4a6xfSk1

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
@antiv
antiv merged commit 7527e28 into main Sep 2, 2026
1 check passed
@antiv
antiv deleted the claude/ai-act-disclosure branch September 3, 2026 07:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants