Skip to content

Transparency — the AI-interaction notice

The User Portal renders an AI-interaction notice before the first substantive turn of every conversation, and the Core Proxy seals that it did. It is a framework-neutral capability: EU AI Act Art 50(1) (applying since 2 August 2026), the Colorado AI Act’s consumer disclosure, Korea’s advance-notice duty and ISO/IEC 42001 A.8.2 all cite the same evidence.

  • A banner at the top of a new conversation (dismissible, shown again for every new conversation), and/or
  • an inline marker as the first line of the first assistant turn — rendered in the pending slot before that turn exists, so the first request can wait for it.

Which one is the configured surface: banner, inline or both (default both). The default text, translated into all eight portal languages, is:

You’re interacting with an AI system. Its responses are generated by AI and may be inaccurate or incomplete, so check anything important.

Configured text per locale overrides it (exact tag first, then the base language). The notice text is rendered verbatim as one text node — its SHA-256 is what gets sealed, so no markup, truncation or interpolation may sit between the digested string and the one the user reads.

The notice never silently disappears. A failed or malformed config response falls back to enabled, both surfaces, default text. The only way to render no notice is a declared exemption together with enabled: false.

  1. Each conversation gets a conversation handle: a fresh ULID, never derived from the session or the user. Every chat request carries it as X-Brutor-Conversation-Handle, and the core stores it on the audit row.

  2. When the notice component has committed to the DOM, the portal computes SHA-256 over the UTF-8 bytes of the exact text shown and calls POST /v1/portal/evidence/notice — once per conversation and method, however often the component mounts.

  3. The conversation’s first chat request is gated: it leaves only after the notice POST has been handed to the network (it does not wait for the response), so the core sees the two in that order.

  4. The core seals the notice record. When an inference record is minted for that conversation, it references the conversation’s earliest notice record with purpose responds_to.

The notice.before_first_turn statement then checks, per conversation, that the notice record precedes the first assistant-turn record both in time and in log order, and reports n of m conversations. A late notice is not met.

{
"schema": "ai.brutor/action-record/v1",
"action_id": "ai.brutor.notice/01K5W8Q9D3V7Z2M4N6P8R0T1X3",
"kind": "fyi",
"verdict": "recorded",
"operator": "default",
"system": { "ai_system_id": "rg-claims-assistant", "contract_version": 4, "contract_hash": "01bd…7cec" },
"timestamp": "2026-09-19T10:00:00.000Z",
"controls": [{ "id": "ai.brutor.notice.rendered", "result": "pass", "blocking": false, "kind": "transparency" }],
"context": {
"record_type": "ai.brutor.notice", "surface": "portal",
"conversation_handle": "01K5W8Q9D3V7Z2M4N6P8R0T1X3", "locale": "en", "method": "banner"
},
"references": [
{ "type": "ai.brutor.notice-text", "digest_alg": "SHA-256",
"digest": "4f2c…9a10", "purpose": "rendered_text" }
],
"record_id": "…"
}

Nothing from the portal JWT enters the record — the authenticated user is used only for rate limiting (30 notices per user per minute).

Compliance → Transparency configures the notice per tenant (the default) and per AI System (an override). Resolution: system row → tenant row → the built-in default.

Field Values
enabled true (default)
surface banner | inline | both
texts {locale: text}, up to 32 locales, 2 000 characters each; text_digests are computed (SHA-256 of the UTF-8 text)
exemption {asserted, basis, rationale} — basis obvious_from_context (Art 50(1): obvious to a reasonably well-informed person), authorised_by_law, no_natural_person, or other; rationale up to 4 000 characters

Every save (and delete) seals an ai.brutor.notice-config record whose payload digest covers the configuration and whose references carry each locale’s text digest. An exemption is a declaration: it is sealed as its own ai.brutor.declaration record and shown as DECLARED — never as evidence that the notice was unnecessary.

The Transparency page also shows the notice evidence (n of m conversations), the notice text declaration and the synthetic-content declaration for the selected system, each through the same resolvers the framework obligations use.

  • Every assistant message in the portal carries an AI-generated label.
  • Chat exports mark assistant turns “Assistant (AI-generated)” and open with a preamble saying so.
  • Successful LLM responses from the gateway carry the header X-Brutor-AI-Generated: true — on /v1/proxy/llm/{chat/completions,responses,completions}, /v1/messages and the Gemini generateContent / streamGenerateContent route; not on embeddings, listings, count_tokens or error responses.
Method Path (Core Proxy, portal JWT) Body → response
GET /v1/portal/evidence/notice-config?ai_system_id= → {enabled, surface, text?: {locale: text}, text_sha256?: {locale: hex}, exemption?}
POST /v1/portal/evidence/notice {conversation_handle, locale, method: "banner"|"inline", text_sha256, ai_system_id?} → {record_id}

Errors: 422 invalid_notice (handle not a ULID, locale not BCP 47-shaped, bad method, digest not 64 lowercase hex, unknown AI System), 429 rate_limited with Retry-After: 60, 503 evidence_disabled. Admin endpoints for configuration are in the API reference.