Aller au contenu

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_received
  • aden_cache_hit
  • aden_no_tables, aden_opa_denied
  • aden_sql_generated, aden_cost_gate, aden_trino_executed
  • aden_pii_masked, aden_dashboard_published
  • aden_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.

  1. Cache lookup — TTL cache (5 min) keyé par sha256(question | rôle). force=true dans la requête le bypass.
  2. Recherche catalog — OpenMetadata _search retourne le top-N des tables candidates pour la question.
  3. Vérification OPA par table — pour chaque candidate, POST à /v1/data/akko/aden/allow avec {user, role, table}. Les tables refusées sont silencieusement retirées du contexte LLM.
  4. 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_id est fourni).
  5. Validation sqlglot — la sortie LLM est parsée en dialecte Trino. Tout ce qui n'est pas un seul SELECT/WITH/UNION/EXCEPT/INTERSECT est rejeté. Une denylist défensive de mots-clés (INSERT/UPDATE/DELETE/…) sert de deuxième ligne de défense. Un LIMIT 10000 est auto-injecté s'il manque.
  6. 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 retourne HTTP 413. L'user peut re-envoyer avec confirm_cost=true pour passer outre.
  7. Exécution Trino — tourne avec le token OAuth de l'appelant, donc les row-filters / column-masks définis ailleurs s'appliquent.
  8. Redaction PII — chaque colonne taggée PII dans 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).
  9. Publication dashboard — un template Jinja rend une page Streamlit standalone /srv/reports/aden-<id>.py et l'écrit sur le PVC partagé. Le sub-chart runtime akko-streamlit l'expose sur reports.<domain>.
  10. Cache + mémoire de session — l'AdenResponse est 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

  1. akko-aden écrit une ligne dans aden.dashboards à chaque query réussie (endpoint /query).
  2. Le Deployment akko-app-catalog-syncer poll cette table toutes les 30 s et rend une app ShinyProxy par ligne dans le ConfigMap akko-shinyproxy-apps.
  3. ShinyProxy hot-reload au changement du ConfigMap (apps-config-folder).
  4. Quand un utilisateur ouvre https://reports.<domain>/app/aden-<id>, ShinyProxy vérifie sa session OIDC, mappe les groupes Keycloak contre les access-groups de 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_URL etc.) pour le test local docker run -e ….
  • Labels NetworkPolicy : app.kubernetes.io/name matche à la fois akko-opa/akko-litellm/akko-keycloak/akko-postgres/akko-tempo ET 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 si akko-opa.adenPoliciesEnabled=true.
  • Bootstrap DB : akko-init crée la base aden et applique aden-schema.sql à chaque helm upgrade. Idempotent.
  • Service user Trino : akko-aden.trino.serviceUser est 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-Id porte le preferred_username Keycloak (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