What you must be able to demonstrate about your AI assistant, and to whom
The question that sooner or later reaches whoever runs an AI assistant in-house is "can we reconstruct what happened?": who asked, to which system, with what outcome, and whether anybody touched the log afterwards. The technical answer is a well-designed event log. The legal answer is less uniform than you read around: an explicit obligation to record exists only for systems classified as high-risk, and for all the others the reason to keep a log comes from the responsibility of the data controller and from common sense, not from an article that imposes it. This article separates the two levels and builds four pieces with programs that were run: a record that does not copy personal data, a pseudonymisation that holds, a chain of fingerprints that makes tampering evident, and an honest calculation of the retention period.
A warning on the perimeter. Below you find what the official texts say, read on 2026-11-13; it is not legal advice and it does not say whether your company has obligations: that depends on what your system does and how it is classified. The case I use is constructed: an SME with an internal assistant that answers on company procedures. For the personal data part, the starting framework is the article on privacy by design; for the security of the log's backups, the one on disaster recovery.
What Article 12 of the AI Act really says
In the consolidated text of Regulation (EU) 2024/1689, version of 27 July 2026, Article 12 is titled "Record-keeping" and says in its first paragraph: "High-risk AI systems shall technically allow for the automatic recording of events (logs) over the lifetime of the system." It is a design requirement addressed to the system: the system must be able to record. The second paragraph says what the records are for, and the third sets a minimum content, but only for the remote biometric identification systems of Annex III point 1(a).
- Identifying situations that may result in the system presenting a risk within the meaning of Article 79(1), or in a substantial modification.
- Facilitating the post-market monitoring referred to in Article 72.
- Monitoring the operation of the system by those who use it, as provided in Article 26(5).
The three purposes are the key to reading: the rule does not ask you to record everything, it asks for records relevant to these three ends, appropriate to the intended purpose of the system. The duration is elsewhere. Article 19 requires providers to keep the logs that are under their control for a period appropriate to the intended purpose, of at least six months; Article 26(6) requires it of whoever uses the system (the deployer, your company if it is the user) with the same threshold of six months. Both provisions add that the logs are kept "unless provided otherwise in the applicable Union or national law, in particular in Union law on the protection of personal data": the minimum of six months does not prevail over the GDPR.
When does it apply? Article 113 of the consolidated text says that Chapter III, Sections 1, 2 and 3, that is Articles 12, 19 and 26 as well, applies from 2 December 2027 for the systems classified as high-risk pursuant to Article 6(2) and Annex III, and from 2 August 2028 for those under Article 6(1) and Annex I. The dates were moved by Regulation (EU) 2026/1744. For an assistant that answers on internal procedures, the upstream question is whether it falls within the Annex III list: if it does not, it has no Article 12 obligation, and the log remains a choice of prudence.
And in Italy?
Law no. 132 of 23 September 2025 (Legge 23 settembre 2025, n. 132) is in Italian, so what follows is my own rendering, not an official translation. Article 1 provides that its provisions are interpreted and applied in conformity with Regulation 2024/1689, and Article 20 designates the Agenzia per l'Italia digitale (AgID, the Agency for Digital Italy) and the Agenzia per la cybersicurezza nazionale (ACN, the National Cybersecurity Agency) as the national authorities for artificial intelligence, without prejudice to the role of market surveillance authority in their respective sectors attributed to Banca d'Italia, CONSOB and IVASS. The text assigns to AgID the promotion of innovation and the functions of notification, assessment, accreditation and monitoring of the bodies that verify conformity, and to ACN the supervision, including inspection and sanctioning activities. Article 24 delegates the Government to adopt, within twelve months of the entry into force (10 October 2025), the legislative decrees of alignment.
Two consequences follow for what you read about "AgID inspections of SMEs with fines of a few thousand euro": the text I read does not give AgID a sanctioning power towards businesses, and I have not verified whether the implementing decrees have been issued, so I report no amounts or sanction procedures. Before setting a deadline, open the text in force on Normattiva and look for the implementing decrees.
What to record, and what not
Here is the design tension: the log serves to reconstruct events, but the GDPR asks to process only the necessary data (minimisation, Article 5(1)(c)) and to keep them only for the necessary time (point (e)). The text of a question to a company assistant may contain names, amounts, a colleague's health matters. If you copy it into the log, you have created a second archive of personal data, with its own access and deletion rules. The prudent choice is to record the event, not the content: who (in pseudonymised form), when, which system and model version, which sources were consulted, how long it took, with what outcome, and a fingerprint of the prompt and of the answer that lets you verify a text later without having copied it.
The program builds such a record and subjects it to an automatic check: every expected field must be there with the right type, no unexpected field must appear, and no plain-text email address may be found in the record. The fingerprint is computed with HMAC and a key, the reason is in the next section. The text of prompt and answer does not enter the record: if a case requires keeping it, it is an exception to be justified with a basis and a separate duration. In the second experiment the wrong record has the email in place of the pseudonym, a field with the prompt text and the outcome missing: the check finds all three.
"""A log record that records the event without copying personal data, with an automatic check."""
import hashlib
import hmac
import json
import re
CHIAVE = b"test-key-to-keep-out-of-the-log"
EMAIL = re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+")
CAMPI = {"event_id": str, "timestamp_utc": str, "system": str, "model_version": str,
"user": str, "prompt_fingerprint": str, "answer_fingerprint": str,
"sources": list, "duration_ms": int, "outcome": str}
def impronta(testo):
return hmac.new(CHIAVE, testo.encode(), hashlib.sha256).hexdigest()[:24]
def costruisci_record(id_evento, istante, utente_email, prompt, risposta, fonti, durata_ms, esito):
# The log receives fingerprints and identifiers, not the text of prompt and answer
return {
"event_id": id_evento,
"timestamp_utc": istante,
"system": "internal-assistant",
"model_version": "model-x 1.0",
"user": impronta(utente_email),
"prompt_fingerprint": impronta(prompt),
"answer_fingerprint": impronta(risposta),
"sources": fonti,
"duration_ms": durata_ms,
"outcome": esito,
}
def controlla(record):
problemi = []
for campo, tipo in CAMPI.items():
if campo not in record:
problemi.append(f"missing {campo}")
elif not isinstance(record[campo], tipo):
problemi.append(f"{campo} is not {tipo.__name__}")
for extra in set(record) - set(CAMPI):
problemi.append(f"unexpected field: {extra}")
testo = json.dumps(record, ensure_ascii=False)
if EMAIL.search(testo):
problemi.append("contains a plain-text email address")
return sorted(problemi)
ok = costruisci_record("evt-0001", "2026-11-13T09:15:02Z", "anna.rossi@example.com",
"What is the expense reimbursement procedure?", "The procedure provides for...",
["kb-101", "kb-205"], 1830, "ok")
print(json.dumps(ok, ensure_ascii=False, indent=2, sort_keys=True))
print("check:", controlla(ok) or "no problem")
errato = dict(ok)
errato["user"] = "anna.rossi@example.com"
errato["prompt"] = "plain text that should not be there"
del errato["outcome"]
print("check of the wrong record:")
for p in controlla(errato):
print(" ", p)
# Output:
# {
# "answer_fingerprint": "0bc04063e94f837d67e26f41",
# "duration_ms": 1830,
# "event_id": "evt-0001",
# "model_version": "model-x 1.0",
# "outcome": "ok",
# "prompt_fingerprint": "da2453b662e552dbb639fc48",
# "sources": [
# "kb-101",
# "kb-205"
# ],
# "system": "internal-assistant",
# "timestamp_utc": "2026-11-13T09:15:02Z",
# "user": "fc7824e032e78358c1585dbb"
# }
# check: no problem
# check of the wrong record:
# contains a plain-text email address
# missing outcome
# unexpected field: prompt
The output shows what really ends up in the log: identifiers, the UTC timestamp, the model and its version, the identifiers of the sources, the outcome and the three fingerprints, and nothing that lets you read the question. For an investigation you can ask whoever wrote it to show the prompt, compute the fingerprint and compare it with the recorded one. The check does not replace review: a rule for emails does not catch a tax code or a name in a free field, and that is why free fields are forbidden. The model_version field is what lets you explain, after an update, why the same prompt gave a different answer.
Really pseudonymising: a plain fingerprint is not enough
The widespread shortcut is to record a SHA-256 hash of the email in place of the email. It works until somebody tries to invert it, and inverting it is easy when the space of possible inputs is small: company addresses follow a pattern, and whoever reads the log can try all the plausible combinations and compare. The program shows the attack on three addresses of a constructed SME. With plain SHA-256 all three are recovered; with HMAC, that is the same hash function but tied to a secret key, whoever does not have the key recovers nothing. The pseudonym stays stable between two computations, so you can still search for a person's events in the log.
"""Pseudonymising the identity in the log: plain SHA-256 against HMAC with a secret key."""
import hashlib
import hmac
# Employees of a constructed SME: the addresses follow the pattern first.last@example.com
DIPENDENTI = ["anna.rossi@example.com", "luca.bianchi@example.com", "sara.verdi@example.com"]
NOMI = ["anna", "luca", "sara", "marco", "paola"]
COGNOMI = ["rossi", "bianchi", "verdi", "neri", "gallo"]
CHIAVE = b"test-key-to-keep-out-of-the-log" # in production it lives in a secrets manager
def sha_semplice(email):
return hashlib.sha256(email.encode()).hexdigest()[:16]
def hmac_chiave(email, chiave):
return hmac.new(chiave, email.encode(), hashlib.sha256).hexdigest()[:16]
def attacco_a_dizionario(pseudonimi, funzione):
"""Whoever reads the log tries all the plausible addresses and compares the pseudonyms."""
trovati = {}
for nome in NOMI:
for cognome in COGNOMI:
ipotesi = f"{nome}.{cognome}@example.com"
if funzione(ipotesi) in pseudonimi:
trovati[funzione(ipotesi)] = ipotesi
return trovati
reg_semplice = {sha_semplice(e) for e in DIPENDENTI}
print("plain SHA-256, addresses recovered from the log:", len(attacco_a_dizionario(reg_semplice, sha_semplice)), "out of", len(DIPENDENTI))
reg_hmac = {hmac_chiave(e, CHIAVE) for e in DIPENDENTI}
# Whoever does not have the key tries to replicate the pattern without the key, or with a wrong key
sbagliata = lambda e: hmac_chiave(e, b"guessed-key")
print("HMAC, without the key, addresses recovered: ", len(attacco_a_dizionario(reg_hmac, sbagliata)), "out of", len(DIPENDENTI))
# Same input and same key give the same pseudonym: the log stays searchable by user
print("stable between two computations:", hmac_chiave(DIPENDENTI[0], CHIAVE) == hmac_chiave(DIPENDENTI[0], CHIAVE))
print("pseudonym of", DIPENDENTI[0], "->", hmac_chiave(DIPENDENTI[0], CHIAVE))
# Output:
# plain SHA-256, addresses recovered from the log: 3 out of 3
# HMAC, without the key, addresses recovered: 0 out of 3
# stable between two computations: True
# pseudonym of anna.rossi@example.com -> fc7824e032e78358
The difference is all in the line that calls hmac.new(chiave, ...). The key must be outside the log and outside the system that reads it, in a secrets manager with tracked access; if it sits in the same place as the log, the attack becomes possible again. Changing the key breaks the link between old and new events: it is a rotation choice to be decided beforehand, because it changes the possibility of following a person over time.
One last clarification, because the opposite idea circulates: pseudonymising does not make data anonymous. Recital 26 of the GDPR says: "Personal data which have undergone pseudonymisation, which could be attributed to a natural person by the use of additional information should be considered to be information on an identifiable natural person." So the log events remain personal data, with all the data subjects' rights. For an erasure request it is not enough to delete the correspondence table and declare the case closed. Article 17(3) excludes the erasure obligation to the extent that processing is necessary for compliance with a legal obligation, or for the establishment, exercise or defence of legal claims: evaluate case by case, and document the evaluation.
A log that makes tampering evident
A log counts as evidence only if you can say that nobody modified it afterwards. A simple and verifiable method is the fingerprint chain: every entry contains the fingerprint of the previous one, computed together with its own content in canonical form (sorted keys, no superfluous spaces, otherwise two equivalent writings give different fingerprints). If somebody modifies an entry, its fingerprint no longer matches the content; if they delete one in the middle, the link between its neighbours breaks.
The program builds a chain of five events and tries four situations. The last one must be read carefully: if somebody deletes the entries at the tail, the chain that remains is still internally consistent, and without an external reference it notices nothing. That is why the head of the chain (the fingerprint of the last entry) and the length must be kept elsewhere, in a place that whoever writes in the log cannot modify: a message sent to someone else's mailbox, a separate read-only log, a file in the backup. With the anchor, the truncation is detected.
"""Fingerprint chain for an append-only log: detects modifications and deletions, and the limit on truncation at the tail."""
import hashlib
import json
GENESI = "0" * 64
def canonico(evento):
return json.dumps(evento, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
def aggiungi(catena, evento):
precedente = catena[-1]["impronta"] if catena else GENESI
impronta = hashlib.sha256((precedente + canonico(evento)).encode()).hexdigest()
return catena + [{"evento": evento, "precedente": precedente, "impronta": impronta}]
def verifica(catena, testa_attesa=None, lunghezza_attesa=None):
"""Returns the list of problems. If head and length are anchored externally, it also checks the tail."""
problemi, precedente = [], GENESI
for i, voce in enumerate(catena):
if voce["precedente"] != precedente:
problemi.append(f"entry {i}: the link with the previous one is broken")
calcolata = hashlib.sha256((voce["precedente"] + canonico(voce["evento"])).encode()).hexdigest()
if calcolata != voce["impronta"]:
problemi.append(f"entry {i}: content modified")
precedente = voce["impronta"]
if lunghezza_attesa is not None and len(catena) != lunghezza_attesa:
problemi.append(f"length {len(catena)} instead of {lunghezza_attesa}")
if testa_attesa is not None and precedente != testa_attesa:
problemi.append("the chain head does not match the anchored one")
return problemi
catena = []
for n in range(1, 6):
catena = aggiungi(catena, {"id": n, "user": f"u{n % 2}", "outcome": "ok"})
testa, lunghezza = catena[-1]["impronta"], len(catena) # to be kept OUTSIDE the log
print("intact:", verifica(catena, testa, lunghezza) or "no problem")
modificata = [dict(v) for v in catena]
modificata[1] = {**modificata[1], "evento": {"id": 2, "user": "u0", "outcome": "denied"}}
print("event 2 modified:", verifica(modificata, testa, lunghezza))
senza_terza = catena[:2] + catena[3:]
print("entry 3 deleted:", verifica(senza_terza, testa, lunghezza))
troncata = catena[:3]
print("last two deleted, without anchor:", verifica(troncata) or "no problem")
print("last two deleted, with anchor: ", verifica(troncata, testa, lunghezza))
# Output:
# intact: no problem
# event 2 modified: ['entry 1: content modified']
# entry 3 deleted: ['entry 2: the link with the previous one is broken', 'length 4 instead of 5']
# last two deleted, without anchor: no problem
# last two deleted, with anchor: ['length 3 instead of 5', 'the chain head does not match the anchored one']
In the output, the modification of the second entry is localised ("entry 1"), the deletion of the third is detected by the broken link and by the length, and the truncation at the tail goes unnoticed without the anchor and is discovered with it. The general limit: whoever has write access to the whole log can recompute the entire chain after modifying an event, so the chain protects against partial tampering and clumsy tampering, not against whoever also controls the anchor. Merkle trees and digital signatures with a timestamp move the limit, at the cost of more complexity; I did not implement them here. For services that offer write-only storage, verify the real guarantees in the provider's documentation before counting on them.
How long to keep: the arithmetic and the purpose
The duration is not a number to copy: it comes from the purpose of the log and from the constraint that personal data are not kept longer than necessary. For a high-risk system there is a minimum of six months (Articles 19 and 26), for all the others no minimum and the necessity limit of the GDPR. The number "seven years", which is often found in guides on the subject, I did not find in either of the two texts as an obligation for the log of an AI assistant; if a specific obligation in your sector imposes it (for example in the financial sector, where the AI Act refers to the documentation already provided for by sector legislation), you find it there and you document it.
A concrete error to avoid is the arithmetic of hours in configuration files. The program shows that 2555 hours are 106.5 days, not seven years: seven years of 365 days are 61,320 hours. A wrong conversion in a retention parameter produces a log that deletes itself after three and a half months without anybody noticing. The second part computes the date on which a record can be deleted and shows a litigation hold: if an investigation is under way, deletion is postponed, and the rule must be written into the system, not entrusted to memory.
"""How long a retention lasts: arithmetic of hours and deletion date with a litigation hold."""
from datetime import date, timedelta
def ore(giorni):
return giorni * 24
print("2555 hours =", round(2555 / 24, 1), "days")
print("seven years of 365 days =", ore(7 * 365), "hours")
print("183 days =", ore(183), "hours")
def data_cancellazione(evento, giorni_conservazione, blocco_fino=None):
"""Date after which the record can be deleted. A hold (investigation, litigation) postpones it."""
scadenza = evento + timedelta(days=giorni_conservazione)
if blocco_fino and blocco_fino > scadenza:
return blocco_fino
return scadenza
evento = date(2026, 11, 13)
print("event", evento, "with 183 days ->", data_cancellazione(evento, 183))
print("same event with a hold until 2027-12-01 ->", data_cancellazione(evento, 183, date(2027, 12, 1)))
# Output:
# 2555 hours = 106.5 days
# seven years of 365 days = 61320 hours
# 183 days = 4392 hours
# event 2026-11-13 with 183 days -> 2027-05-15
# same event with a hold until 2027-12-01 -> 2027-12-01
If the log lives in Grafana Loki, the Loki documentation says that retention is obtained with the compactor, that by default the retention_enabled flag is not active and so logs live forever, and that the Table Manager with its retention is deprecated. The following configuration is a schema to adapt and was not run: no Loki was started for this article, and the value of 4392 hours (183 days) is only a calculation example.
# Schema to adapt, NOT run: no Loki was started. Parameters from the Loki documentation on retention.
compactor:
working_directory: /loki/compactor
retention_enabled: true # without this flag, by default logs stay forever
delete_request_store: filesystem # mandatory when retention is active
limits_config:
retention_period: 4392h # 183 days, only as an example: the purpose of the log decides the duration
The schema contains two pitfalls. The first is in the comment on retention_enabled: leaving the default value, the log is never deleted, and this is a minimisation problem, not just a space problem. The second is the constraint that delete_request_store be set when retention is active, as the documentation says. Remember too that if you have a lifecycle policy on the object store, it must last longer than the retention period, otherwise the store deletes first.
Who reads the log, and how you test it
Reading the log is itself processing: those who access it must be few, identified, and their accesses must be recorded elsewhere. Verifying the chain and checking the records are done at regular intervals, not on the day somebody asks for them: the same logic as the restore drill in the previous article. A reasonable routine is to verify the chain against the anchor every day, and every quarter to try to reconstruct the history of an event starting from the log, the way an auditor would. If the reconstruction takes more than a few minutes or a field is missing, you have found the defect before whoever would have challenged you with it.
Frequently asked questions
Do I have to keep a log even if my system is not high-risk?
Article 12 of the AI Act does not oblige you, because it concerns high-risk systems. The GDPR remains: if you process personal data you must be able to demonstrate compliance with the principles (Article 5(2)) and to guarantee security, and a minimal log is the ordinary way of doing so.
How long must I keep the logs?
For high-risk systems at least six months (Articles 19 and 26), unless provided otherwise by the applicable Union or national law, in particular on the protection of personal data. For all the others there is no minimum: purpose and necessity count.
Is a hash of the email enough to pseudonymise?
No: if the addresses follow a predictable pattern, a dictionary attack recovers them, as the program shows. You need an HMAC with a secret key kept outside the log, and even so the data remain personal.
Can I record the text of the prompts?
You can, but you are creating a second archive of personal data, with minimisation and duration to justify. The prudent choice is to record fingerprints and source identifiers, and to keep the text only in justified cases.
What you can do now
You can say what Article 12 of the AI Act requires, whom it addresses, from when it applies and where its minimum duration stops in front of the GDPR. You can build a record that describes the event without copying personal data and verify it with an automatic check, pseudonymise with a key instead of a plain hash, make tampering evident with a fingerprint chain anchored externally and compute retention without unit errors. The next step in the series reverses the perspective: when it is better to run nothing in-house.
The same series also includes the recap with the decision sheet.
Sources
- Regulation (EU) 2024/1689, consolidated text of 27.7.2026 - Articles 12, 19, 26 and 113 (English text), consulted on 2026-11-13
- Regulation (EU) 2026/1744 (Digital Omnibus on AI) - amendment of the application dates, consulted on 2026-11-13
- Regulation (EU) 2016/679 (GDPR) - Articles 5 and 17, recital 26 (English text), consulted on 2026-11-13
- Law of 23 September 2025, no. 132 (Legge 23 settembre 2025, n. 132) on Normattiva - Articles 1, 20 and 24; the text is in Italian, the renderings are my own, consulted on 2026-11-13
- Grafana Loki - Log retention - compactor, retention_enabled, deprecated Table Manager, consulted on 2026-11-13
The programs in the article were run with Python 3.14.4, twice each with identical output; users, addresses, events and keys are constructed examples. The Loki schema was not run. Not verified: whether the legislative decrees provided for by Article 24 of Law 132/2025 have been issued.

Commenti
Caricamento commenti...
Sign in to comment