Cette page reprend
docs/SECURITY.md, qui fait autorité sur le modèle
de sécurité V1 (sprint 6). Le ton reste volontairement le même :
honnête sur ce qui est fait et sur ce qui ne l’est pas encore.Modèle d’auth : clés API par projet
Une clé (hk_<hex>) est liée à un org_id + un project_id.
Seul le hash sha256 est stocké (table api_keys, migration 0006) ; la
clé en clair n’est retournée qu’à la création. Le prefix (8 premiers
caractères) sert aux affichages masqués.
HAKI_AUTH_REQUIRED=true(défaut) : tout endpoint/v1/*(sauf gestion des clés) et/gateway/v1/*exigeAuthorization: Bearer hk_.... Clé absente, invalide ou révoquée → 401unauthorized.- Liaison de scope : si le corps ou la query contient un
project_iddifférent de celui de la clé → 403forbidden_scope, message générique, aucun indice sur l’existence d’autres projets. Vérifié sur capture (project_idde chaque événement), context, feedback, forget, resolve (corps) et timeline, inspect, conflicts (query). /gateway/v1/*: même middleware, même clé. Le corps chat-completions ne porte pas deproject_id: le scope mémoire est celui de la clé, sans exception. La clé Haki n’est jamais transmise au provider LLM — l’appel en amont utilise uniquement les credentialsHAKI_LLM_*côté serveur.HAKI_AUTH_REQUIRED=false= mode dev ouvert, documenté, jamais en production : avertissement explicite loggué au démarrage (haki.main).
Gestion des clés
Règles d’accès (V1, volontairement simples) :
HAKI_ADMIN_KEYdéfini → mode admin : toute la gestion des clés exigeAuthorization: Bearer <HAKI_ADMIN_KEY>.- Non défini → bootstrap documenté : la première création
est libre (table vide). Ensuite, une clé valide gère les clés de son
propre projet — une clé d’un autre projet renvoie le même 404
key_not_foundqu’un id inconnu, sans fuite.
Policy Engine V1 (app/policy/)
Module déterministe (pas de LLM), appelé avant l’action par
capture, context, forget et par le middleware d’auth. Trois règles en V1
(pas de règles custom utilisateur — sprint ultérieur) :
1
Scope présent
subject_id non vide sur chaque événement capturé (missing_scope,
cohérent avec le Ledger).2
Clé ↔ projet
La liaison de scope ci-dessus (403
forbidden_scope).3
purpose recommandé sur context
Warning
missing_purpose dans le packet (et la trace persistée),
pas une erreur en V1.haki.policy, policy_decision {...}) ; chaque oubli est audité.
Erreurs typées : unauthorized, forbidden_scope, missing_scope,
jamais de révélation cross-projet.
Row-Level Security (migration 0006)
RLS activé +FORCE ROW LEVEL SECURITY sur events, facts,
context_traces, conflict_sets, policy haki_project_isolation :
- La dépendance
get_sessionposeSELECT set_config('haki.project_id', :pid, true)(SET LOCAL, portée transaction) depuis la clé résolue par l’auth. Garantie PRD : une requête qui oublie le filtreproject_iddans le code ne voit que les lignes du projet de la clé — prouvée partests/test_rls.py(SELECTsans.where,INSERTcross-projet rejeté par leWITH CHECK). - Mode dev ouvert : pas de
SET, GUCNULL→ policy permissive. C’est aussi le mode du worker interne et du serveur MCP (project et subject fixés par config serveur — aucun des deux outilshaki_*n’accepte desubject_iden paramètre). NULLIF(..., '')est indispensable : après unSET LOCALannulé en fin de transaction, Postgres laisse le GUC custom à''(pasNULL), et les connexions mutualisées (pool) le réutilisent — sans ça, toute connexion ayant servi une requête authentifiée cachait toutes les lignes (bug trouvé en démo live, test de régressiontest_rls_empty_string_setting_is_permissive).- Deux rôles : les migrations tournent avec le rôle propriétaire
haki(DDL) ; le runtime utilisehaki_app(créé par la migration, mot de passehaki— credential de dev local à remplacer en déploiement), qui n’est ni superuser ni propriétaire. Un superuser contourne RLS même avecFORCE: sans ce rôle dédié la garantie serait factice. Config :HAKI_DATABASE_URL(runtime,haki_app) etHAKI_MIGRATION_DATABASE_URL(alembic,haki). /v1/consolidatereste un endpoint dev/ops cross-projets : session sans contexte RLS (get_session_ops), documenté.
Feedback et résolution de conflits
POST /v1/feedback : un rating="incorrect" sur un fact_id transitionne
le fait vers disputed — le Context Assembler ne le sert plus comme
actif. POST /v1/conflicts/{id}/resolve tranche une contradiction avec
historique complet (l’ancien devient superseded, jamais supprimé). Voir
Feedback et
Conflits.
Secrets
La clé LLM vit dans.env (ignoré par git, modèle fourni dans
.env.example), jamais dans le code, le terminal ou le frontend.
Preuve multilingue
scripts/check_multilingual.py (serveur lancé avec
HAKI_LLM_PROVIDER=openai, embedder local
paraphrase-multilingual-MiniLM-L12-v2) : capture FR + EN + ES pour un
sujet, consolidation, requêtes croisées (EN→FR, FR→ES, ES→EN) —
3/3 PASS au dernier run documenté. Les predicate/value extraits
restent en anglais technique quelle que soit la langue d’entrée : voulu
et documenté — la langue d’entrée ne contraint pas le schéma de faits,
seule compte la fidélité sémantique.

