Le pod SearXNG crashait en boucle : granian recevait --port tcp://<ip>:8080
au lieu d'un entier. Cause classique k8s : le Service « searxng » fait injecter
l'env service-link SEARXNG_PORT=tcp://<clusterIP>:8080, que l'entrypoint SearXNG
lit comme port de bind.
- enableServiceLinks:false (coupe l'injection des env service-link, inutiles ici)
- SEARXNG_PORT=8080 + SEARXNG_BIND_ADDRESS=0.0.0.0 explicites (défense en profondeur)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Asa ne répondait qu'à partir d'un contexte figé (RAG doc ou blocs live
pré-câblés, à présélectionner) → les questions à état live (« gpu-01 tourne
bien ? ») tombaient sur « la doc ne le précise pas ». Bascule vers une boucle
de function-calling : le modèle décide quels outils appeler puis répond à
partir de leurs résultats.
- Nouveau contexte `asa` (STT_DEFAULT_CONTEXT=asa, défaut prod) + tools.py
(registre schémas OpenAI + exécuteurs) + brain.ask_with_tools (boucle bornée
STT_TOOL_MAX_ITERS=4, réponse forcée au-delà ; _post factorisé + retry).
- Outils Phase 1, LECTURE SEULE : search_docs (RAG funk-docs), host_health
(gpu-01|storage-01 : up/charge/RAM + llama-server), cluster_status,
prometheus_query (PromQL arbitraire). Réutilisent sources.py / knowledge.py.
- Trace des outils renvoyée dans `context` → visualiseur HUD (un bloc par appel).
- 100 % local (Qwen3-8B) : tool-calling natif llama.cpp validé, survit au
/no_think. LiteLLM transmet bien les tools. PromQL validés contre le vrai
Prometheus in-cluster. 3 tests unitaires de la boucle (hors-ligne).
Web search (SearXNG in-cluster) = Phase 2 ; actions admin pilotées par le LLM
(hermes-exec comme outil) = Phase 3.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(stt): active les actions via Hermes (STT_ACTIONS_ENABLED=true)
hermes-exec déployé sur storage-01 (:9096) + secret k8s stt-server-secrets/hermes-exec-token
en place → on active le contexte « agent ». Après déploiement ArgoCD, le contexte
🤖 Agent apparaît dans le sélecteur et les actions (avec confirmation) sont opérationnelles.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: journal 2026-06-21 + état STT (portail, contextes, intents, actions Hermes)
- progress/2026-06-21.md : récap complet de la journée (portail + santé live, contextes
visualiseur + presets live, intents vocaux, jeton Ghostfolio unifié, actions via Hermes,
ASR Parakeet) + l'incident de merge des PR empilées et sa leçon.
- PROGRESS.md : ligne du jour.
- CLAUDE.md : « État actuel » daté 2026-06-21, ligne STT enrichie.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(vault): ajoute vault_hermes_exec_token (jeton actions hermes-exec, chiffré)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Asa peut AGIR sur le homelab (l'autre moitié de la vision), via le vrai agent Hermes,
profil par défaut, tous ses outils — avec confirmation et jeton.
Découverte : Hermes n'a pas d'API HTTP (appli TUI), mais un mode one-shot `hermes -z
"<prompt>" --yolo`. On s'appuie dessus.
storage-01 — exécuteur :
- tools/hermes-exec/server.py : service HTTP qui lance `hermes -z --yolo` en user hermes,
derrière un jeton Bearer (compare_digest), timeout + audit, une action à la fois.
- rôle Ansible hermes_exec : systemd (User=hermes, env hermes-agent), jeton via
EnvironmentFile 0640 (Vault vault_hermes_exec_token) ; ajouté au playbook storage-01.
STT-server (0.5.0 → 0.6.0) :
- agent.py : pont vers hermes-exec (jeton) + détection confirme/annule.
- contexts.py : contexte « agent » (court-circuite le LLM).
- app.py : flux dédié — handshake 2 temps par session (pending action) → sur « confirme »,
appelle hermes-exec ; « annule » annule. /v1/contexts masque « agent » si désactivé.
- config : STT_ACTIONS_ENABLED (opt-in, défaut false) + URL + jeton (secret k8s).
- deployment : env actions + secret stt-server-secrets/hermes-exec-token (optionnel).
Sécurité : opt-in désactivé par défaut ; jeton obligatoire (sinon contexte caché + exécuteur
refuse tout) ; --yolo atteint seulement jeton+confirmation en main ; audit storage-01.
Client : AUCUN changement (le contexte « agent » apparaît tout seul dans le sélecteur).
Validé en local : exécuteur (401 sans/mauvais jeton, exec avec bon jeton via echo),
handshake serveur (TestClient : confirme→exécute, annule→annule, agent masqué si OFF).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Asa n'est plus bloqué sur le seul contexte « doc cluster grounding-strict ». Le client
choisit un contexte par requête ; le serveur change le system prompt ET injecte les
données live du domaine, puis renvoie le contexte assemblé pour le visualiseur du HUD.
- contexts.py : profils funk / ghostfolio / grafana / alerting / cluster (system prompt
+ sources) + assemble() (prompt final + structure de visualisation).
- sources.py : fetchers live best-effort (Ghostfolio auth+details, Alertmanager alerts
hors Watchdog, Prometheus cluster/metrics), env-config, dégradation propre.
- brain.py : ask() reçoit le system prompt déjà assemblé (assemblage remonté).
- app.py : /v1/ask accepte `context`, renvoie context_id + le contexte assemblé ;
nouveau GET /v1/contexts ; RAG doc conditionné au profil.
- config.py : URLs sources + STT_GHOSTFOLIO_TOKEN + STT_DEFAULT_CONTEXT.
- deployment : env in-cluster (Prometheus/Alertmanager monitoring, Ghostfolio ai),
jeton via secret optionnel stt-server-secrets/ghostfolio-token.
- bump 0.3.1 → 0.4.0.
Validé en local : assemblage (blocs+RAG+mémoire), parsing des sources (mock),
endpoints /v1/contexts et /v1/ask (LLM mocké) — context_id, visualiseur, fallback
contexte inconnu → funk.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(stt): mémoire long-terme sémantique via Qdrant (5b)
Serveur : longterm.py — collection Qdrant stt-memory (embeddings Qwen3 gpu-01, dim auto,
Cosine), recall top-k injecté au prompt, remember des tours user. Tout dégrade proprement
si Qdrant/embeddings injoignables (la mémoire court-terme tient). Env STT_MEMORY_LONGTERM,
STT_QDRANT_URL, STT_EMBED_URL, STT_MEMORY_TOPK.
Testé en process : dégradation OK (Qdrant down → mem=0, pas de crash, court-terme tient).
Qdrant réparé le 17/06 (5c). Recherche sémantique réelle à valider sur cluster.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013FmcxGsyXZXogiAHQLjnZT
* feat(stt): endpoint /v1/memory/health + upsert Qdrant synchrone
- /v1/memory/health sonde activement embeddings + Qdrant + collection et
expose les erreurs (recall/remember dégradent en silence → indébogables).
Permet de diagnostiquer la mémoire long-terme sans kubectl exec.
- remember() : upsert avec ?wait=true → le souvenir est immédiatement
cherchable (sans wait, Qdrant met l'écriture en file → un recall
cross-session immédiat pouvait le rater).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013FmcxGsyXZXogiAHQLjnZT
* docs(stt): 5b mémoire long-terme validée en prod + backlog nomic-embed-text
- Rappel cross-session confirmé (« Felix » retrouvé dans une nouvelle session),
points_count vérifié via /v1/memory/health.
- Note du fix upsert ?wait=true et de l'endpoint de diagnostic.
- Roadmap : 5d (nomic-embed-text dim 768) en backlog qualité ; états haut/bas
du doc mis à jour (déployé + validé sur cible).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013FmcxGsyXZXogiAHQLjnZT
* feat(stt): embeddings dédiés nomic-embed-text sur gpu-01 (:1238) + migration auto
Remplace Qwen3 (chat réutilisé, dim 4096, peu discriminant) par un modèle
d'embedding spécialisé nomic-embed-text (dim 768) pour la mémoire long-terme.
Ansible (rôle llama_server) :
- nouvelle instance optionnelle `llama-embed` (llama_embed_enabled) servant un
modèle d'embedding dédié sur :1238, GPU ; télécharge le GGUF si absent.
- activée sur gpu-01 (host_vars) : nomic-embed-text-v1.5 f16.
STT-server :
- STT_EMBED_URL → :1238, STT_EMBED_MODEL → nomic-embed-text (deployment + config).
- _ensure_collection détecte le changement de dimension (4096→768) et recrée
automatiquement la collection stt-memory (anciens vecteurs incomparables) —
pas de drop manuel.
Docs : llama_server README, rag.md, stt.md (5d ✅), CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013FmcxGsyXZXogiAHQLjnZT
---------
Co-authored-by: Claude <noreply@anthropic.com>
Serveur : longterm.py — collection Qdrant stt-memory (embeddings Qwen3 gpu-01, dim auto,
Cosine), recall top-k injecté au prompt, remember des tours user. Tout dégrade proprement
si Qdrant/embeddings injoignables (la mémoire court-terme tient). Env STT_MEMORY_LONGTERM,
STT_QDRANT_URL, STT_EMBED_URL, STT_MEMORY_TOPK.
Testé en process : dégradation OK (Qdrant down → mem=0, pas de crash, court-terme tient).
Qdrant réparé le 17/06 (5c). Recherche sémantique réelle à valider sur cluster.
Claude-Session: https://claude.ai/code/session_013FmcxGsyXZXogiAHQLjnZT
Co-authored-by: Claude <noreply@anthropic.com>
La CI build l'image en sha-<commit> et, sur main, met à jour k8s/apps/stt/deployment.yaml
avec ce tag (commit [skip ci]). ArgoCD redéploie alors une image immuable et déterministe
— fini les races de timing et les 'kubectl rollout restart' manuels.
permissions: contents write (pour le bump). :latest gardé en parallèle sur main (fallback).
Claude-Session: https://claude.ai/code/session_013FmcxGsyXZXogiAHQLjnZT
Co-authored-by: Claude <noreply@anthropic.com>
L'indirection litellm-ext (Service sans sélecteur + Endpoints manuel) ne routait pas
('All connection attempts failed'). open-webui joint LiteLLM en IP directe — on copie
ce pattern éprouvé et on supprime litellm-external.yaml.
Claude-Session: https://claude.ai/code/session_013FmcxGsyXZXogiAHQLjnZT
Co-authored-by: Claude <noreply@anthropic.com>
* fix(stt): imagePullSecret ghcr-pull pour l'image privée
Le Deployment référence un secret de pull ghcr-pull (ns ai) — image ghcr privée.
Secret créé manuellement (même pratique que ghostfolio-secret, non versionné).
Doc d'install dans stt/README.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013FmcxGsyXZXogiAHQLjnZT
* fix(stt): securityContext conforme PodSecurity restricted (ns ai)
runAsNonRoot + runAsUser 1000 + seccompProfile RuntimeDefault (pod),
allowPrivilegeEscalation false + drop ALL caps (conteneur).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013FmcxGsyXZXogiAHQLjnZT
---------
Co-authored-by: Claude <noreply@anthropic.com>
La devise interne est figée à USD en v3.11 (BASE_CURRENCY non pris en compte) ;
EUR devient disponible via la collecte du taux EUR/USD, pas via cet env.
La devise d'affichage se choisit par utilisateur dans Paramètres.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Avoirs majoritairement en euros — fixe la devise de base système à l'installation
pour un affichage natif en EUR sans dépendre d'un taux de change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Nouvel app ArgoCD ghostfolio (namespace ai) : suivi/analyse de portefeuille
boursier. Base PostgreSQL dédiée sur storage-01, Redis in-cluster pour le cache.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Open WebUI (openwebui.lab.local) :
- Connecté à LiteLLM (192.168.10.1:4000/v1) comme backend OpenAI-compatible
- PostgreSQL via storage-01 pour l'historique des conversations
- PVC 5Gi NFS pour les uploads, Traefik IngressRoute
n8n (n8n.lab.local) :
- PostgreSQL via storage-01, chiffrement des credentials
- PVC 2Gi NFS, Traefik IngressRoute
- Webhook URL interne : http://n8n.lab.local
Ansible : ajoute openwebui et n8n dans postgresql_databases/users
(vault_pg_openwebui_password + vault_pg_n8n_password à ajouter au vault)
Prérequis avant 1er déploiement :
make vault-edit # ajouter les 2 passwords
make apply-storage --tags postgresql
kubectl create secret generic open-webui-secret -n ai ...
kubectl create secret generic n8n-secret -n ai ...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
instance=~"192\.168\.10\.(11|12|13):9100" → job="node-exporter"
La barre oblique inversée dans les regex PromQL string literals causait
une erreur "unknown escape sequence" (\. n'est pas un escape PromQL valide).
job="node-exporter" est équivalent (seuls les 3 compute nodes ont ce job)
et ne dépend pas d'adresses IP hardcodées.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
nftables.conf.j2 :
- Ajoute 10.42.0.0/16 (CIDR pods k8s) pour port 9100 (node_exporter)
et port 8080 (hermes webhook AlertManager)
- Appliqué live sur storage-01 + persisté dans /etc/sysconfig/nftables.conf
- storage-01 était up=0 (timeout) car pods Prometheus ne pouvaient pas
l'atteindre malgré node_exporter actif
dashboard-infrastructure.yaml :
- Corrige mountpoint /srv → /srv/data (RAID5 monté sur /srv/data)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Grafana 13 était OOMKilled régulièrement (exit code 137, 5 restarts
observés) avec la limite à 256Mi — cause des "no available server"
Traefik pendant les redémarrages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
L'Application monitoring ne scannait que le répertoire racine,
ignorant les sous-dossiers dashboards/ et alerts/.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Le chart kube-prometheus-stack génère une sous-route pour l'alerte
Watchdog qui référence le receiver "null". Notre config le remplaçait
sans le définir → ReconciliationFailed, StatefulSet jamais créé.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>