ADEN — Autonomous Data Exploration Node¶
ADEN transforme une question en langage naturel en SQL Trino, l'exécute avec l'identité de l'appelant, puis publie un dashboard Streamlit. C'est l'équivalent souverain open source de Databricks Genie, Snowflake Copilot et Dremio Text-to-SQL — avec une grosse différence : aucune donnée ne quitte ton cluster.
| Comparaison | ADEN | Databricks Genie | Snowflake Copilot |
|---|---|---|---|
| Self-hosted, OSS | ✓ | ✗ | ✗ |
| RBAC enforcement | OPA | Unity Catalog | Snowflake Roles |
| Redaction PII | ✓ | ✓ | ✓ |
| Cost gate EXPLAIN | ✓ | ✗ | ✗ |
| Publish dashboard | Streamlit | Genie session | Aucun |
| Mémoire convers. | ✓ | ✓ | ✓ |
| 100 % offline | ✓ | ✗ | ✗ |
Architecture¶
Cockpit /api/cockpit/aden/ (nginx forward-auth)
│
▼
docker/aden (FastAPI)
│ │ │ │
│ │ │ └──> OPA /v1/data/akko/aden/allow
│ │ └──────> OpenMetadata /api/v1/tables/... (lookup tags PII)
│ └──────────> LiteLLM → Ollama (qwen2.5-coder:7b)
└──────────────> Trino /v1/statement (token user, RBAC appliqué)
│
▼
PVC akko-aden-reports (RWO)
│
▼
docker/streamlit (montage RO) — reports.<domain>
Chaque étape est loggée en JSON structuré dans logs layer via audit_log().
Les events à filtrer dans Dashboards → logs layer :
aden_query_receivedaden_cache_hitaden_no_tables,aden_opa_deniedaden_sql_generated,aden_cost_gate,aden_trino_executedaden_pii_masked,aden_dashboard_publishedaden_feedback
Pipeline de sécurité¶
Chaque requête ADEN passe par un pipeline strict avant qu'aucune query n'atteigne Trino. L'ordre est intentionnel — chaque étape suppose que la précédente a réussi.
- Cache lookup — TTL cache (5 min) keyé par sha256(question | rôle).
force=truedans la requête le bypass. - Recherche catalog — OpenMetadata
_searchretourne le top-N des tables candidates pour la question. - Vérification OPA par table — pour chaque candidate, POST à
/v1/data/akko/aden/allowavec{user, role, table}. Les tables refusées sont silencieusement retirées du contexte LLM. - Génération LLM — qwen2.5-coder:7b reçoit le system prompt + les
tables autorisées + les 3 derniers tours de session (si un
session_idest fourni). - Validation
sqlglot— la sortie LLM est parsée en dialecte Trino. Tout ce qui n'est pas un seulSELECT/WITH/UNION/EXCEPT/INTERSECTest rejeté. Une denylist défensive de mots-clés (INSERT/UPDATE/DELETE/…) sert de deuxième ligne de défense. UnLIMIT 10000est auto-injecté s'il manque. EXPLAIN (TYPE IO)— Trino estime les bytes scannés. Si l'estimé dépasse la limite (défaut 1 GiB,AKKO_COST_BYTES_LIMIT), la requête retourneHTTP 413. L'user peut re-envoyer avecconfirm_cost=truepour passer outre.- Exécution Trino — tourne avec le token OAuth de l'appelant, donc les row-filters / column-masks définis ailleurs s'appliquent.
- Redaction PII — chaque colonne taggée
PIIdans OpenMetadata est remplacée par***dans les preview rows ET le dashboard persisté, sauf si le rôle est dans la bypass list (défaut : admin + engineer). - Publication dashboard — un template Jinja rend une page Streamlit
standalone
/srv/reports/aden-<id>.pyet l'écrit sur le PVC partagé. Le sub-chart runtimeakko-streamlitl'expose surreports.<domain>. - Cache + mémoire de session — l'
AdenResponseest stockée sous la clé de cache, et l'historique de conversation est complété pour les follow-ups.
Partage & runtime multi-tenant (Sprint 25)¶
Chaque dashboard appartient à l'utilisateur qui l'a créé. Le propriétaire change sa visibilité depuis la page ADEN du cockpit → modal Share… :
| Visibilité | Qui peut ouvrir le dashboard |
|---|---|
private |
Seulement le propriétaire (les admins overrident toujours) |
users |
Propriétaire + chaque adresse dans allowed_users[] |
groups |
Propriétaire + tout membre d'un groupe Keycloak dans allowed_groups[] |
public |
Tout utilisateur AKKO authentifié sauf akko-viewer |
La décision est appliquée côté serveur par le package OPA
akko.aden_share (voir helm/akko/charts/akko-opa/templates/configmap.yaml).
Le cockpit récupère la matrice via GET /dashboards?scope=mine|shared|public|all
et ne se repose jamais sur un filtrage côté client.
Pourquoi ShinyProxy¶
Les métadonnées de visibilité en Postgres ne règlent que la découverte.
Elles n'empêchent pas deux utilisateurs de partager un interpréteur
Python Streamlit — un dashboard malveillant pourrait lire le
st.session_state du suivant. Il faut une isolation physique.
ShinyProxy (Apache 2.0, déployé via le sub-chart akko-shinyproxy)
spawn un container neuf par (utilisateur × dashboard) et route le
navigateur vers ce container. L'état est par container, le cycle de vie
par session, et une NetworkPolicy verrouille l'egress du pod à Trino +
DNS uniquement — pas de lecture des pods voisins, pas d'internet, pas
de Postgres en dehors du schéma ADEN.
Flow du catalog d'apps¶
akko-adenécrit une ligne dansaden.dashboardsà chaque query réussie (endpoint/query).- Le Deployment
akko-app-catalog-syncerpoll cette table toutes les 30 s et rend une app ShinyProxy par ligne dans le ConfigMapakko-shinyproxy-apps. - ShinyProxy hot-reload au changement du ConfigMap
(
apps-config-folder). - Quand un utilisateur ouvre
https://reports.<domain>/app/aden-<id>, ShinyProxy vérifie sa session OIDC, mappe les groupes Keycloak contre lesaccess-groupsde l'app, puis spawn le pod.
Signature dashboard (HMAC-SHA256)¶
Comme Streamlit exécute du Python arbitraire depuis le PVC, ADEN signe
chaque fichier dashboard avec HMAC-SHA256 de AKKO_DASHBOARD_HMAC_KEY
avant de l'écrire. La ligne de tête # AKKO_SIG: <hex> est vérifiée au
lancement — un fichier modifié est refusé.
Rotation de clé : positionner AKKO_DASHBOARD_HMAC_KEY_OLD à l'ancienne
clé avant de changer l'active. Les deux clés valident pendant la
transition ; retirer _OLD une fois que chaque dashboard a été re-signé
par son owner (ou passé la période de rétention configurée).
Configuration¶
Tous les boutons opérationnels sont des env vars sur le deployment
akko-aden, donc aucun changement de code n'est nécessaire pour ajuster
les gates de sécurité :
| Env var | Défaut | Rôle |
|---|---|---|
AKKO_SQL_AUTO_LIMIT |
10000 |
LIMIT auto-injecté |
AKKO_COST_BYTES_LIMIT |
1 GiB |
Seuil du gate EXPLAIN |
AKKO_COST_GATE |
true |
Désactiver le gate (tests) |
AKKO_QUERY_CACHE_TTL |
300 |
TTL cache en secondes |
AKKO_QUERY_CACHE_SIZE |
256 |
Entrées cache |
AKKO_SESSION_TTL |
3600 |
TTL historique conversation |
AKKO_PII_BYPASS_ROLES |
akko-admin,akko-engineer |
Rôles voyant le PII brut |
AKKO_PII_REDACTION |
*** |
Chaîne de remplacement |
Le mapping rôle → tables vit dans le configmap OPA sous
aden_access.json, dans le bloc akko_aden_access.roles. Pour ouvrir
un nouveau schéma aux analysts, éditer ce fichier et helm upgrade —
aucun changement de code.
API REST¶
POST /query¶
Headers : X-User-Id, X-User-Role, X-User-Token.
{
"question": "top 10 clients par CA en 2025",
"session_id": "s-abcd1234",
"execute": true,
"confirm_cost": false,
"force": false,
"persist": true
}
La réponse contient sql, columns, preview_rows, row_count,
dashboard_url, reasoning, masked_columns, cost_bytes, cached,
session_history_size.
Le champ optionnel model outrepasse le défaut serveur
(AKKO_LLM_MODEL) pour cette requête uniquement. La valeur doit
correspondre à un model_name déclaré dans LiteLLM (GET /models) ;
toute valeur inconnue est rejetée par un 400 Unknown LLM model.
GET /models¶
Retourne les modèles LLM accessibles à ADEN et le défaut courant :
{
"default": "akko-chat",
"models": ["akko-chat", "akko-fast", "akko-coder", "claude-sonnet"],
"details": [...]
}
Adossé à LiteLLM /v1/models (résultats memoïsés 60 s). Le menu
"Modèle IA" du cockpit, au-dessus du chat ADEN, est peuplé via cet
endpoint ; le dernier choix de l'utilisateur est conservé en
localStorage.
Pour ajouter un nouveau provider (Claude, OpenAI, Mistral, une
seconde instance Ollama, etc.), ajoutez une entrée dans
akko-litellm.config.model_list de votre values file puis lancez un
helm upgrade — aucune modification de code n'est nécessaire :
akko-litellm:
config:
model_list:
- model_name: claude-sonnet
litellm_params:
model: anthropic/claude-sonnet-4-6
api_key: os.environ/ANTHROPIC_API_KEY
- model_name: mistral-on-prem
litellm_params:
model: openai/mistral
api_base: http://internal-mistral.acme.local:8000/v1
api_key: os.environ/MISTRAL_KEY
Les nouvelles entrées apparaissent dans le picker au prochain rafraîchissement du cockpit (cache 60 s).
POST /feedback¶
Capture thumbs up/down par dashboard. Servira au futur dashboard qualité et à seed les datasets de prompt tuning.
GET /session/{session_id}¶
Retourne l'historique de conversation pour l'utilisateur appelant. Le cockpit appelle ça au reload pour restaurer le panneau de chat.
GET /catalog?q=<mots-clés>¶
Cherche dans OpenMetadata puis bascule en fallback Trino multi-catalog
(tpch, tpcds, postgresql, iceberg par défaut — override via
AKKO_ADEN_FALLBACK_CATALOGS). Retourne fqn, name, description,
columns (remplies via system.jdbc.columns pour que le LLM reçoive
un vrai schéma).
GET /dashboards?scope={mine,shared,public,all}¶
Liste les dashboards visibles pour l'appelant. Utilisé par les onglets "Mes dashboards", "Partagés avec moi", "Publics" du cockpit.
PATCH /dashboards/{id}/share¶
Met à jour la visibilité + listes d'autorisation. Body :
{
"visibility": "public | users | groups | private",
"allowed_users": ["bob@akko.local"],
"allowed_groups": ["akko-analyst"]
}
Owner-only (les non-owners reçoivent 404 pour ne pas leaker l'existence).
DELETE /dashboards/{id}¶
Owner ou utilisateur autorisé. Supprime la ligne DB (le .py généré
sur le PVC est GC'd par le prochain tick janitor).
GET /dashboards/{id}/access¶
Décision OPA allow/deny. Utilisé par ShinyProxy avant de spawn un pod
Streamlit pour un appelant non-owner. Retourne {allow: bool, reason: string}.
Observabilité¶
Le tracing est actif quand AKKO_TRACING_ENABLED=true (par défaut sur
Netcup). Les spans OTLP atterrissent dans Tempo sous service.name=
akko-aden. Le dashboard Dashboards akko-aden-slo affiche p50/p95/p99,
taux de succès, consommation du budget d'erreur (fenêtres 1h/6h/1j).
/metrics est exposé par prometheus-fastapi-instrumentator :
aden_query_duration_seconds_bucket (histogramme) + aden_query_total
(compteur) sont les séries que le dashboard SLO interroge.
Pièges opérationnels (trouvés 2026-04-16 sur Netcup)¶
- Préfixe env var : le Helm passe
AKKO_TRINO_URL,AKKO_LITELLM_URL,AKKO_OPA_URL,AKKO_OPENMETADATA_URL,AKKO_REPORTS_DIR. Le code Python lit d'abord ces noms puis retombe sur les anciens (TRINO_URLetc.) pour le test localdocker run -e …. - Labels NetworkPolicy :
app.kubernetes.io/namematche à la foisakko-opa/akko-litellm/akko-keycloak/akko-postgres/akko-tempoET les équivalents sans préfixe pour rester portable. - ConfigMap OPA : les 3 fichiers ADEN (
aden_access.json,aden_access.rego,aden_share.rego) ne sont montés que siakko-opa.adenPoliciesEnabled=true. - Bootstrap DB :
akko-initcrée la baseadenet appliqueaden-schema.sqlà chaque helm upgrade. Idempotent. - Service user Trino :
akko-aden.trino.serviceUserest le username qu'ADEN utilise pour SES propres requêtes metadata. Doit matcher un utilisateur Keycloak réel qu'OPA/Ranger accepte. Les requêtes utilisateur finales tournent sous l'identité individuelle (X-User-Id). - Header cockpit :
X-User-Idporte lepreferred_usernameKeycloak (pas l'email). La policy OPA Trino matche sur username.
Page cockpit¶
Le cockpit ajoute un item sidebar ADEN visible à partir d'akko-user.
La page contient :
- Une textarea pour la question (Cmd/Ctrl+Entrée pour soumettre).
- Deux checkboxes : Dry run (retourne SQL sans exec) et Confirm cost (override le gate EXPLAIN).
- Un panneau Reasoning qui montre ce qu'ADEN a compris : intent, tables candidates, tables autorisées par OPA, tables effectivement utilisées dans le SQL.
- Une textarea SQL éditable avec estimation de coût.
- Un tableau de Résultats avec row count et badge de redaction PII.
- Boutons 👍 / 👎 + lien vers le dashboard publié.
Voir aussi¶
- LLM RBAC — quotas + matrice rôle ↔ modèle
- OpenMetadata — où vivent les tags PII et le catalog
- Trino — moteur de query, policies OPA Trino
- ADR XXX (prévu) — architecture decision record ADEN