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.
What the user sees
Section titled “What the user sees”- 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.
The ordering guarantee
Section titled “The ordering guarantee”-
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. -
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. -
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.
-
The core seals the notice record. When an inference record is minted for that conversation, it
referencesthe conversation’s earliest notice record with purposeresponds_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.
The sealed notice record
Section titled “The sealed notice record”{ "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).
Configuration and exemptions
Section titled “Configuration and exemptions”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.
AI-generated labelling
Section titled “AI-generated labelling”- 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/messagesand the GeminigenerateContent/streamGenerateContentroute; not on embeddings, listings,count_tokensor 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.

