Stocker les conversations et protéger les traces
En bref
- Pour rendre la réponse lisible,
piighostgarde, pour chaque conversation, les valeurs sensibles trouvées dans chaque message. Ce stockage contient donc des données personnelles. - Les trois lieux de stockage sont la mémoire du programme (perdue au redémarrage), Redis et une base SQL.
- Redis et la base SQL peuvent chiffrer ce qu'ils gardent. Le chiffrement exige deux secrets fournis par l'environnement du serveur.
- Effacer une conversation supprime son stockage. Sur un serveur à plusieurs processus, une copie temporaire peut survivre dans les autres processus tant qu'aucune durée de vie n'est réglée.
- Les traces techniques contiennent par défaut le texte en clair. Un réglage les remplace par des jetons.
Cette page s'adresse surtout aux développeurs et aux exploitants. Le déroulé d'une conversation est décrit dans Suivre une conversation et restaurer la réponse. Les termes sont définis dans le glossaire. La mise en production est décrite dans le guide technique. Déployer un pipeline en production couvre un serveur, et Déploiement multi-instance couvre plusieurs instances derrière un répartiteur de charge. Les garanties de stockage y sont détaillées dans Sécurité.
Choisir un stockage
| Stockage | Clé [memory] type | Survit au redémarrage | Partagé entre processus | Durée de vie |
|---|---|---|---|---|
| Mémoire du programme | in_memory | non | non | ttl par conversation, max_threads (10 000 conversations et un jour par défaut) |
| Redis | redis | oui | oui | ttl par message |
| Base SQL (SQLAlchemy) | sqlalchemy | oui | oui | aucune |
Recommandation : in_memory pour le développement et les tests, Redis ou SQL dès que plusieurs processus servent la même conversation.
Ce qui est stocké
Chaque message est stocké avec son empreinte, c'est-à-dire une courte suite de caractères calculée à partir de son texte, qui sert à le reconnaître sans garder ce texte. Le stockage garde aussi le rôle de son auteur (user ou assistant) et ses détections (position, texte, étiquette, confiance). Le texte des détections est la donnée sensible.
- Redis :
{namespace}:{thread_id}:msg:{empreinte}contient le rôle et les détections.{namespace}:{thread_id}:indexcontient l'ordre d'arrivée des messages (conversation_memory/redis_backend.py:7-12). - SQL : une ligne par message dans
piighost_conversation_messages(id,thread_id,message_digest,role,detections,detection_count).
Le chiffrement d'un stockage repose sur deux composants, toujours configurés ensemble :
- Le hacheur : il calcule l'empreinte de chaque message avec un secret, le poivre (
PIIGHOST_HASH_PEPPER). Sans le poivre, personne ne peut recalculer l'empreinte d'un texte connu pour vérifier s'il a été stocké. - Le chiffreur : il chiffre les détections stockées avec une clé AES (
PIIGHOST_CIPHER_KEY). Sans la clé, les valeurs stockées sont illisibles.
Règles à connaître
BR-STO-01 . Quand vous fournissez un hacheur sans chiffreur, ou un chiffreur sans hacheur, alors la construction échoue. En code, l'erreur est ValueError("Provide both a hasher and a cipher, or neither"). En configuration, c'est ConfigError("Configure both a hasher and a cipher, or neither"). Hacher les clés en laissant les valeurs en clair ne protège rien. 2 emplacements · 2 tests directs
BR-STO-02 . Quand Redis ou une base SQL est construit sans chiffrement, alors un PIIGhostSecurityWarning est émis. Une base SQLite fait exception et ne déclenche pas l'avertissement (conversation_memory/sqlalchemy_backend.py:99-100). 3 emplacements · 12 tests directs
BR-STO-03 . Quand le chiffrement est actif, alors l'identifiant de conversation reste en clair. Il sert de préfixe de clé Redis et de colonne SQL, pour pouvoir lister et effacer une conversation. N'y mettez pas de donnée personnelle (une adresse e-mail, un nom). 2 emplacements · 12 tests directs
BR-STO-04 . Quand la mémoire du programme est créée sans réglage, alors elle garde au plus 10 000 conversations. Au-delà, la moins récemment utilisée est évincée. Chaque conversation expire un jour (86 400 secondes) après sa dernière écriture, et elle est retirée au prochain accès. max_threads et ttl changent ces bornes. Par exemple, une conversation écrite le 2 octobre 2026 à 9 h et plus touchée ensuite est oubliée le 3 octobre 2026 à partir de 9 h. 4 emplacements · 37 tests directs
BR-STO-05 . Quand Redis a un ttl, alors chaque message expire ce nombre de secondes après son écriture. L'index de la conversation reçoit la même durée à chaque nouveau message. La base SQL n'a aucune expiration. Effacez les conversations vous-même. 1 emplacement · 2 tests directs
BR-STO-06 . Quand une conversation est effacée, alors son stockage et le cache de jetons du processus qui reçoit la demande sont vidés. Les autres processus gardent leur cache jusqu'à son éviction (256 cartes au plus) ou jusqu'à token_memo_ttl. Par exemple, sur un serveur à 4 processus, une demande d'effacement reçue par le processus 1 laisse les valeurs dans le cache des processus 2 à 4 tant que token_memo_ttl n'est pas réglé. 2 emplacements · 8 tests directs
BR-STO-07 . Quand la clé de chiffrement ne fait pas 16, 24 ou 32 octets une fois décodée du base64, alors la construction échoue avec InvalidKeyLengthError. 1 emplacement · 7 tests directs
BR-STO-08 . Quand aucun masqueur de traces n'est configuré et qu'un exportateur OpenTelemetry est actif, alors les traces portent le texte en clair. Un PIIGhostSecurityWarning est émis à la construction, sauf si trace_clear_text=True l'acquitte. 1 emplacement
Configurer Redis chiffré
- Installez les extras :
uv add "piighost[redis,crypto,argon2,config]". - Exportez le poivre :
PIIGHOST_HASH_PEPPER(toute chaîne non vide, gardée hors du dépôt). - Exportez la clé :
PIIGHOST_CIPHER_KEY="$(openssl rand -base64 32)". - Partez de
examples/config/thread_redis.toml, qui déclare déjà[memory.hasher]et[memory.cipher]. La section mémoire a cette forme :
[memory]
type = "redis"
url = "redis://localhost:6379/0"
ttl = 86400
[memory.hasher]
type = "argon2"
[memory.cipher]
type = "aesgcm"- Chargez le pipeline avec
load_thread_pipeline("pipeline.toml").
Pour une base SQL, remplacez la section par type = "sqlalchemy", exportez l'URL asynchrone dans PIIGHOST_DATABASE_URL, puis appelez await pipeline.memory.create_schema() une fois au démarrage. Le chargement de la configuration ne crée pas la table.
Vérifier
redis-cli --scan --pattern 'piighost:*'Vous devez voir des clés piighost:<thread_id>:msg:<empreinte>. redis-cli GET sur l'une d'elles doit renvoyer des octets illisibles, pas un JSON contenant les valeurs.
Choisir le hacheur
| Hacheur | type | Coût | Résiste à une fuite du poivre |
|---|---|---|---|
| HMAC-SHA256 | sha256 | rapide | non |
| Argon2id | argon2 | lent, gourmand en mémoire | oui, en partie |
Argon2id prend par défaut time_cost = 2, memory_cost = 19456 Kio, parallelism = 1, hash_length = 32. Le hacheur tourne à chaque message. Mesurez donc la latence avant de durcir ces valeurs.
Masquer les traces
Le pipeline ouvre un span par étape (piighost.detect, piighost.link, piighost.render, etc.) via OpenTelemetry. Un span est une entrée de trace qui mesure une étape et porte ses données. Sans l'extra observation, le traceur ne fait rien. Avec lui, les spans vont au TracerProvider de l'application.
- Sans masqueur, le texte et les valeurs détectées sont tracés en clair. Les traces contiennent alors des données personnelles. Réservez ce mode à un service de traces que vous contrôlez, et acquittez-le par
trace_clear_text=True(BR-STO-08 ). - Avec
observation_redactor(une fabrique de jetons, section[observation_redactor]), les valeurs sont remplacées par des jetons dans les traces. - Le pipeline de conversation pose l'identifiant de conversation dans l'attribut
langfuse.session.id, en clair, même avec un masqueur.
Pièges
- Le cache de jetons n'est pas partagé entre processus. Réglez
token_memo_ttldès que vous effacez des conversations sur un déploiement multi-processus (BR-STO-06 ). - La table SQL n'est pas créée par la configuration. Sans
create_schema(), le premier message échoue. - Deux écritures SQL simultanées du même message peuvent lever une erreur de contrainte d'unicité, parce que la vérification puis l'écriture ne sont pas atomiques (
sqlalchemy_backend.py:124-127). Redis, lui, retente sousWATCH. - Le cache des motifs de mots est commun à tout le processus.
forget_threadne le vide pas. Appelezclear_boundary_cachesi la demande d'effacement couvre tout le processus. - Une clé AES perdue rend la mémoire illisible. Les conversations en cours ne peuvent plus être restaurées.
Où vivent les règles
| Règle | Emplacement |
|---|---|
| BR-STO-01 | conversation_memory/base.py:59-71 (require_paired_crypto), config/models/memory.py:61 (le même refus en configuration) |
| BR-STO-02 | conversation_memory/base.py:43 (warn_plaintext), appelé par conversation_memory/redis_backend.py:105 et conversation_memory/sqlalchemy_backend.py:99-100, qui épargne SQLite |
| BR-STO-03 | conversation_memory/redis_backend.py:107-113 (_index_key, clés préfixées par l'identifiant de conversation), conversation_memory/sqlalchemy_backend.py:92 (colonne thread_id) |
| BR-STO-04 | conversation_memory/memory.py:45-62 (__init__, bornes DEFAULT_MAX_THREADS et DEFAULT_TTL lignes 13 et 20), _expired lignes 140-144, _evict lignes 150-157 |
| BR-STO-05 | conversation_memory/redis_backend.py:148-152 (remember, expiration du message et de l'index) |
| BR-STO-06 | pipeline/thread.py:244-262 (forget_thread), pipeline/thread.py:29 (_TOKEN_MEMO_MAX = 256) |
| BR-STO-07 | crypto/cipher/aesgcm.py:46-52 (AesGcmCipher.__init__) |
| BR-STO-08 | pipeline/base.py:190-202 (l'avertissement de __init__, acquitté par trace_clear_text) |
Tests
| Test | Couvre |
|---|---|
tests/conversation_memory/ | Chaque stockage : isolement des conversations, effacement, durées de vie, avertissement en clair (test_warn_plaintext.py) |
tests/conversation_memory/test_sqlalchemy.py | Création de table, stockage chiffré en SQL |
tests/crypto/ | Longueur de clé AES, poivre vide refusé, déterminisme des hacheurs |
tests/observation/ | Spans émis, masquage de leur contenu, avertissement de traces en clair |
Les tests Redis tournent contre fakeredis (tests/conversation_memory/test_redis.py:21-27), pas contre un vrai serveur. test_concurrent_identical_remembers_do_not_duplicate vérifie l'atomicité sous WATCH avec ce faux client. Le comportement d'un vrai cluster Redis n'est pas couvert.
Voir aussi Configurer un pipeline pour les secrets et la section [memory].