# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Présentation du projet

**Funk** — homelab IA composé d'un cluster Kubernetes (Talos) pour les services applicatifs, d'un hôte d'inférence LLM dédié (gpu-01 / llama-server ROCm), et d'un agent autonome (Hermes Agent) sur storage-01. Stack IaC complète : Ansible + talhelper + ArgoCD.

**État actuel (2026-06-23)** :
- ✅ Hermes Agent opérationnel — multi-profils (funk-ai/system/monitor/brain)
- ✅ Qdrant réparé (2026-06-17) — fin du crash-loop (segment `funk-docs` corrompu, voir `admin/incidents.md`). La collection RAG `funk-docs` a depuis été **reconstruite** (436 chunks, 🟢 green, embeddings nomic-embed-text dim 768) — `rag-query` opérationnel
- ✅ Monitoring complet — Prometheus + Grafana + AlertManager → webhook Hermes + n8n
- ✅ storage-01 installé — `192.168.10.1` (LAN cluster), `192.168.1.200` (WAN)
- ✅ gpu-01 installé — llama-server GPU (:1234, qwen3-8b) + embeddings dédiés nomic-embed-text (:1238, **CPU** — découplé du slot chat GPU) + watchdog/heartbeat de résilience, kernel 5.14.0-687.5.3. ⚠️ Les instances CPU (:1236/:1237 — profils Hermes `system`/`monitor`) sont **manuelles** et souvent éteintes (gap IaC)
- ✅ LiteLLM sur storage-01 — proxy multi-modèles + ask-agent (écoute `0.0.0.0:4000`)
- ✅ Cluster k8s Talos opérationnel (MetalLB + Traefik + wildcard DNS lab.local)
- ✅ postfix_relay — SMTP sortant via Gmail SMTP + App Password (aliyesilkaya93@gmail.com)
- ✅ Open WebUI opérationnel — `openwebui.lab.local`, namespace `ai`, backend LiteLLM
- ✅ n8n opérationnel — `n8n.lab.local`, namespace `ai`, 3 workflows actifs (alertes, rapport horaire, Hermes Doc Worker)
- ✅ STT / **Asa** (assistant vocal « Jarvis ») — STT-server in-cluster (`stt.lab.local`, namespace `ai`) + client `stt` (pipx) : HUD avancé, transcription partielle live, **veille/réveil vocal** (« Asa » / « Asa stop »), auto-start systemd `--user`, mémoire long-terme Qdrant `stt-memory`. **Asa est agentique (2026-06-22)** : contexte **`asa` par défaut** = **boucle d'outils (function calling, Qwen3-8B local)** où le modèle choisit ses outils — `search_docs` (RAG), `host_health`/`cluster_status`/`prometheus_query` (état **live** enrichi : CPU/RAM/disque/temp/uptime + **GPU** via rocm_scraper), **`web_search`** (SearXNG), et **`admin_action`** (agir via `hermes-exec`, **confirmation vocale** obligatoire + jeton). Contextes présélectionnables conservés (funk/ghostfolio/grafana/alerting/cluster) + visualiseur + intents vocaux. **TTS enfichable** (`piper`|`kokoro`) : `stt --voices` / `--install-voice` / `--install-kokoro`. Voir `admin/ia/stt.md` et `docs/stt-presentation.md`
- ✅ SearXNG opérationnel — `searxng.lab.local`, namespace `ai`, **méta-moteur de recherche web self-host** (privé, in-cluster) consommé par l'outil `web_search` d'Asa (API JSON). Manifests `k8s/apps/searxng/`
- ✅ Ghostfolio opérationnel — `ghostfolio.lab.local`, namespace `ai`, suivi de portefeuille (PostgreSQL s01 + Redis in-cluster)
- ✅ hermes-auto-improve — analyse quotidienne de `admin/` par Hermes à 22h (n8n → trigger :9095), PR sur `hermes/daily-work`
- 🆕 Annuaire + portail consoles (2026-07-06) — **Authentik** (`auth.lab.local`, IdP OIDC) + **Guacamole** (`portail.lab.local`, consoles SSH web vers s01/g01 via user `console` sans sudo), namespace `auth`. Config initiale (admin Authentik, provider OIDC, connexions) : voir `admin/k8s/auth-portal.md`. **Accès Internet** : Authentik = hub utilisateur (`kaya.freeboxos.fr:9081`, page « My applications ») → SSO vers Guacamole (`:9080`) — VIPs MetalLB `.201`/`.202`, rate-limit nftables, URL canonique publique pour tous (hairpin LAN)
- 🆕 WireGuard (2026-07-06) — serveur VPN prêt sur s01 (pairs `full`/`portal`, filtrage nftables) mais **dormant** : pas de redirection Freebox `51820/udp` (choix simplicité 2026-07-07, accès portail via `:9080` direct). Voir `admin/infra/wireguard.md`
- ⏳ WOL compute nodes non configuré (BIOS) — allumage manuel requis

Journal de progression : `PROGRESS.md` → `progress/YYYY-MM-DD.md`

---

## Architecture des machines

| Machine | IP | OS | Rôle |
|---|---|---|---|
| storage-01 | 192.168.10.1 / 192.168.1.200 | AlmaLinux 9.8 | Passerelle + bastion admin + données + Hermes Agent — **hors cluster** |
| compute-01 | 192.168.10.11 | Talos v1.13 (k8s v1.33.1) | k8s control-plane |
| compute-02 | 192.168.10.12 | Talos v1.13 (k8s v1.33.1) | k8s worker |
| compute-03 | 192.168.10.13 | Talos v1.13 (k8s v1.33.1) | k8s worker |
| gpu-01 | 192.168.10.20 | AlmaLinux 9.8 | llama-server dédié (RX 6700XT, ROCm 7.x) — **hors cluster** |

> 📸 État live vérifié (services, ports, workloads, bases, RAG) : **`admin/ops/etat-cluster.md`** (snapshot 2026-06-21).

**Points clés d'architecture :**
- `storage-01` et `gpu-01` sont **hors du cluster k8s** — consommés via `ExternalName` services ou `Endpoints` manuels
- Single control-plane assumé : si compute-01 tombe, `kubectl apply` ne fonctionne plus mais les workloads continuent
- Modèles LLM sur storage-01 (`/srv/data/models`, NVMe depuis 2026-05-29), montés sur gpu-01 via NFS (`/mnt/models`)

---

## Stack IaC — trois outils, trois cibles

| Outil | Cible | Déclenchement |
|---|---|---|
| **Ansible** | storage-01, gpu-01 (AlmaLinux) | Push manuel : `make apply-storage` / `make apply-gpu` |
| **talhelper** | compute-01/02/03 (Talos) | Push manuel : `talosctl apply-config` |
| **ArgoCD** | Workloads k8s | Pull automatique depuis Git |

---

## Ansible

### Commandes via Makefile (depuis la racine du repo)

```bash
python3 -m venv .venv  # créer le venv (une seule fois, avant make install)
make install        # pip install + ansible-galaxy (à faire une fois)
make ping           # vérifier la connectivité vers tous les hôtes
make check          # dry-run site.yml (storage-01 + gpu-01)
make apply          # appliquer site.yml complet
make apply-storage  # appliquer uniquement storage-01.yml
make apply-gpu      # appliquer uniquement gpu-01.yml
make vault-view     # lire le vault chiffré
make vault-edit     # éditer le vault (ouvre $EDITOR)
make vault-encrypt  # (ré)chiffrer vault.yml
```

### Commandes directes (depuis `ansible/`)

```bash
ansible-playbook -i inventory.yml playbooks/storage-01.yml --tags litellm
ansible-playbook -i inventory.yml playbooks/gpu-01.yml --tags llama_server
```

Les tags correspondent aux noms de rôles. Tous les rôles sont idempotents.

### Inventaire et connectivité

- `storage-01` : accès direct SSH via `192.168.1.200` (WAN)
- `gpu-01` : accès via ProxyJump sur `storage-01` (configuré dans `inventory.yml`)
- Vault password dans `.vault_pass` (ignoré par git, chargé automatiquement via `ansible.cfg`)

### Rôles — storage-01

`hermes_user`, `common`, `gateway` (nftables + NAT), `dnsmasq`, `nfs_server`, `qdrant`, `postgresql`, `minio` (⚠️ rôle vide, service non déployé), `litellm`, `hermes_agent`, `rag`, `node_exporter`, `alertmanager_webhook`, `postfix_relay`, `hermes_auto_improve` (worker doc Hermes + trigger server :9095), `hermes_exec` (exécuteur d'actions vocales `hermes -z --yolo` derrière jeton, :9096), `console_user` (user `console` sans sudo pour le portail Guacamole), `wireguard` (accès distant `51820/udp`, pairs full/portal — voir `admin/infra/wireguard.md`)

### Rôles — gpu-01

`hermes_user`, `common`, `rocm`, `nfs_client`, `llama_server`, `node_exporter`, `satisfactory_server` (serveur dédié Satisfactory `:7777`, steamcmd + systemd), `console_user` (idem storage-01)

> Chaque rôle a un `README.md` (variables, caveats, gaps IaC).
> `lm_studio` est présent dans `ansible/roles/` mais n'est plus utilisé dans aucun playbook (déprécié, voir son README).
> ⚠️ Gap IaC : les instances llama-server CPU `:1236`/`:1237` sur gpu-01 sont des services **manuels**, non gérés par le rôle `llama_server` (voir `ansible/roles/llama_server/README.md`).

### Caveat premier déploiement — hermes_agent

Le rôle `hermes_agent` installe le binaire via `curl | bash` seulement si le binaire est absent. La configuration (`.env`, `config.yaml`) est ensuite conditionnelle à `hermes_binary.stat.exists`. Sur un hôte vierge, il faut **deux runs consécutifs** : le premier installe, le second configure.

### Variables par hôte

`ansible/host_vars/<hostname>/vars.yml` contient les variables spécifiques à chaque machine :
- `storage-01` : UUID RAID5, exports NFS, chemins des services sous `/srv/data`
- `gpu-01` : chemin du modèle GGUF (`llama_model_path`), alias modèle, `llama_ctx_size`, montages NFS

Variables globales (non-secrètes) dans `ansible/group_vars/all/vars.yml` : `admin_user`, `dns_domain`, `ntp_servers`, `cluster_network`, `wan_gateway`.

### Secrets Ansible Vault

Variables chiffrées dans `ansible/group_vars/all/vault.yml` :
- `vault_anthropic_api_key` — clé Anthropic pour le profil brain
- `vault_pg_hermes_password`, `vault_pg_litellm_password`, `vault_pg_n8n_password`, `vault_pg_openwebui_password`, `vault_pg_ghostfolio_password`
- `vault_postfix_relay_password` — App Password Gmail (relay SMTP)
- `vault_n8n_api_key` — clé API n8n (label "claude") pour gérer les workflows via `/api/v1`
- `vault_hermes_exec_token` — jeton d'auth de `hermes-exec` (actions vocales via Hermes ; même valeur dans le secret k8s `stt-server-secrets/hermes-exec-token`)
- `vault_pg_authentik_password`, `vault_pg_guacamole_password` — bases PG du portail (mêmes valeurs dans les secrets k8s `auth/authentik-secret` et `auth/guacamole-secret`)
- `vault_console_ssh_private_key` — clé privée SSH des connexions Guacamole (user `console` sur s01/g01)
- `vault_authentik_secret_key`, `vault_authentik_redis_password` — copies de sûreté des valeurs du secret k8s `auth/authentik-secret`
- `vault_wireguard_server_private_key`, `vault_wireguard_peer_private_keys` — clés WireGuard (serveur + dict par pair)

---

## talhelper (nœuds Talos)

```bash
cd talos/
talhelper genconfig                                        # régénérer configs nœuds
talhelper gencommand apply --node compute-02 | bash        # appliquer sur un nœud
talhelper gencommand bootstrap --node compute-01 | bash    # bootstrap (1ère fois seulement)
talhelper gencommand kubeconfig | bash                     # récupérer kubeconfig
```

`talos/clusterconfig/` est généré — gitignored, ne pas committer.
`talos/talsecret.sops.yaml` chiffré avec SOPS+age (clé dans `.config/sops/`, ignorée par git).

---

## ArgoCD (workloads k8s)

Pattern "App of Apps" : modifier un manifest → `git commit` + `git push` → ArgoCD poll automatiquement (~3 min).

```bash
git add k8s/apps/open-webui/
git commit -m "open-webui: bump to v0.4.0"
git push
```

ArgoCD utilise le repo `git@github.com:Alkatrazz24/Funk-lab.git` branche `main`.

### Structure k8s/

```
k8s/
├── apps-of-apps/
│   ├── root.yaml            # Application ArgoCD racine (pointe vers apps-of-apps/apps/)
│   └── apps/                # Un fichier Application par workload — ArgoCD les crée automatiquement
│       ├── n8n.yaml
│       ├── open-webui.yaml
│       ├── stt.yaml
│       ├── searxng.yaml
│       ├── ghostfolio.yaml
│       ├── openalice.yaml
│       ├── finlab.yaml
│       ├── authentik.yaml
│       ├── guacamole.yaml
│       ├── monitoring.yaml
│       └── nfs-provisioner.yaml
├── apps/                    # Manifests raw k8s par application
│   ├── n8n/                 # namespace: ai
│   ├── open-webui/          # namespace: ai
│   ├── stt/                 # namespace: ai — STT-server (assistant vocal Asa, stt.lab.local)
│   ├── searxng/             # namespace: ai — méta-moteur recherche web (outil web_search d'Asa)
│   ├── ghostfolio/          # namespace: ai — suivi de portefeuille (ghostfolio.lab.local)
│   ├── openalice/           # namespace: ai — agent IA financier (openalice.lab.local, back-end finance d'Asa) — ⏸️ SUSPENDU
│   ├── finlab/              # namespace: ai — dashboard finance + console Claude Code (finance.lab.local, /console) + toolkit
│   ├── authentik/           # namespace: auth — annuaire/IdP OIDC (auth.lab.local)
│   ├── guacamole/           # namespace: auth — portail consoles SSH web s01/g01 (portail.lab.local)
│   └── {external-services}/ # ExternalName/Endpoints pointant vers storage-01 ou gpu-01
└── infra/                   # Helm releases (HelmRelease + values.yaml)
    ├── monitoring/           # kube-prometheus-stack 85.0.2
    └── nfs-provisioner/
# ⚠️ Traefik + MetalLB ne sont PAS dans k8s/infra/ : ils sont installés via `helm install`
#    lors du bootstrap Talos (voir admin/k8s/talos.md), hors pattern App-of-Apps ArgoCD.
```

### Ajouter un nouveau workload k8s

1. Créer `k8s/apps/<nom>/` avec les manifests (Deployment, Service, Ingress, PVC…)
2. Créer `k8s/apps-of-apps/apps/<nom>.yaml` (ArgoCD Application CRD) — copier `n8n.yaml` comme modèle
3. `git commit` + `git push` → ArgoCD prend en charge automatiquement

Namespaces actifs : `argocd`, `infra` (Traefik, MetalLB, NFS-provisioner, registry in-cluster), `monitoring` (Prometheus/Grafana), `ai` (n8n, open-webui, stt, searxng, ghostfolio, openalice, finlab), `auth` (Authentik, Guacamole), `sacrifice` (jeu FPS — déployé hors de ce repo et hors ArgoCD, cf. `admin/ops/etat-cluster.md`).

> Les services du namespace `ai` sont exposés via des **IngressRoute Traefik** (CRD), pas des `Ingress` standards.

---

## Stack IA — Hermes Agent

### Profils

| Profil | Modèle | Port | Usage |
|---|---|---|---|
| `funk-ai` | Qwen3-8B Q4_K_M | GPU :1234 | Agent principal (~35 tok/s) |
| `system` | Qwen3-1.7B Q4_K_M | CPU :1236 | Calculs, formatage, résumés |
| `monitor` | Qwen3-1.7B Q4_K_M | CPU :1237 | Analyse logs, métriques, état services |
| `brain` | Claude Sonnet 4.6 API | Anthropic | Raisonnement complexe — facturé |

Hermes écoute sur le port `8080` (gateway). Dashboard web sur le port `9119` (accessible uniquement depuis `192.168.1.10`).

### Switcher le modèle de Hermes à chaud

```bash
hermes-switch status    # modèle actuel de hermes-default
hermes-switch qwen      # → Qwen3-8B sur gpu-01 (local, gratuit)
hermes-switch claude    # → Claude Sonnet 4.6 via API Anthropic (facturé)
```

`hermes-switch` modifie `/etc/litellm/config.yaml` puis redémarre `litellm`. L'alias `hermes-default` dans LiteLLM est ce que Hermes consomme — changer cet alias ne nécessite pas de reconfigurer Hermes.

### ask-agent (délégation inter-agents)

Binaire dans `/usr/local/bin/ask-agent` sur storage-01. Appel LiteLLM direct (:4000), réponse en ~2-5s.

**Règle** : ask-agent reçoit du **texte**, jamais des commandes shell. Toujours 2 étapes :
```bash
# Étape 1 — collecter les données
journalctl -u litellm -n 20 --no-pager --output=cat
# Étape 2 — passer le texte à ask-agent
ask-agent monitor "logs litellm : <résultat étape 1> — erreurs ?"
```

### RAG — Documentation Funk

> ✅ **`funk-docs` reconstruite et opérationnelle** (relevé 2026-06-21 : **436 chunks**, 🟢 green). Le warning « collection supprimée le 2026-06-17, re-ingest requis » est **périmé** : elle a été ré-indexée depuis. Qdrant et la mémoire long-terme STT (`stt-memory`) sont opérationnels.

Qdrant collection `funk-docs` indexée depuis `admin/` (436 chunks, embeddings **nomic-embed-text** `:1238` dim 768 — plus qwen3-8b ; score min ajusté pour nomic dans `rag-query`).

```bash
rag-query "comment relancer dnsmasq ?"
rag-query "nftables port cluster configuration"
rag-query "hermes profils litellm modèles" --top 3
```

Scripts installés sur storage-01 : `/usr/local/bin/rag-query`, `/usr/local/bin/rag-ingest`

Après modification de `admin/`, re-déployer via la commande directe (voir section Ansible) :
```bash
ansible-playbook -i inventory.yml playbooks/storage-01.yml --tags rag
```
Le script `admin/index_knowledge.py` est l'ingesteur Python brut.

### Hermes Skills — `hermes-skills/`

Versionnés dans ce repo, déployés par le rôle `hermes_agent` vers `~hermes/`.

```
hermes-skills/
├── funk/
│   ├── agent-delegation/SKILL.md   # usage de ask-agent (2 étapes, agents dispo)
│   └── rag-docs/SKILL.md           # usage de rag-query
└── souls/                          # prompts système par profil
    ├── funk-ai.md    # profil principal (agent sur storage-01)
    ├── system.md     # profil calculs/formatage
    ├── monitor.md    # profil analyse logs
    └── brain.md      # profil Claude API
```

### hermes-auto-improve — analyse quotidienne de la doc

Worker qui fait analyser `admin/` par Hermes et pousse les rapports sur la branche `hermes/daily-work` (PR squash-only vers `main`). Source dans `tools/hermes-auto-improve/`, déployé par le rôle `hermes_auto_improve` :

- `/usr/local/bin/hermes-auto-improve` — script d'analyse (`--all` = tous les docs, sinon batch de 5 en rotation ; `--daily-pr` = crée/met à jour la PR GitHub)
- `/usr/local/bin/hermes-auto-improve-server` — trigger HTTP `:9095` (`POST /trigger/all`, `/trigger/daily-pr`, `GET /status`)
- Scheduling : workflow n8n « Hermes Doc Worker — Funk-lab » à **22h** (run complet → boucle de polling `/status` → email + PR). Pas de timer systemd.
- Rapports écrits dans `admin/hermes/builtin/`, état dans `/var/lib/hermes-auto-improve/`, log `/var/log/hermes-auto-improve.log`

Doc détaillée : `admin/ia/hermes-auto-improve.md`

---

## Base de connaissance — `admin/`

Documentation admin versionnée, indexée dans le RAG :

```
admin/
├── ia/           # hermes, litellm, llama_server, rocm, rag, alertmanager-webhook, hermes-auto-improve
├── infra/        # dnsmasq, nfs, réseau, ssh, email
├── install/      # procédures installation storage-01, gpu-01, k8s
├── k8s/          # argocd, talos, monitoring, n8n, open-webui, k9s
├── ops/          # ansible, cluster, système
├── hermes/builtin/  # rapports générés par hermes-auto-improve (ne pas éditer)
└── incidents.md  # historique des incidents matériel/service avec résolutions
```

---

## Réseaux

- CNI Flannel — Pods : `10.42.0.0/16`
- Services : `10.43.0.0/16`
- MetalLB pool : `192.168.10.200-230` (IP virtuelles Traefik/services)
- DNS wildcard `*.lab.local` via dnsmasq → Traefik

nftables géré par le rôle `gateway` sur storage-01. Modifier `ansible/roles/gateway/templates/nftables.conf.j2` puis :
```bash
ansible-playbook -i inventory.yml playbooks/storage-01.yml --tags gateway
```

---

## Contrainte GPU critique

La RX 6700XT (gfx1031) n'est **pas officiellement supportée** par ROCm. Variable obligatoire dans tous les services GPU :

```
HSA_OVERRIDE_GFX_VERSION=10.3.0
```

Gérée automatiquement par le rôle `llama_server`. ROCm tourne directement sur l'hôte AlmaLinux.

---

## Gestion du cluster

Ces commandes sont des **fonctions bash sur le poste perso** (configurées dans `~/.bashrc`) qui SSH sur storage-01 et invoquent `/usr/local/bin/funk-cluster`.

```bash
funk-status                 # état de tous les services (storage-01 + gpu-01 + k8s)
funk-start                  # WOL gpu-01 + démarrage services storage-01
funk-start --k8s            # idem + nœuds k8s (WOL → drain-aware boot)
funk-stop                   # arrêt propre gpu-01 + services
funk-stop --k8s             # idem + drain k8s → NFS check → talosctl shutdown
```

**etcd backup** (single control-plane → critique) :
```bash
talosctl etcd snapshot /path/to/backup.db --nodes compute-01
```

---

## Conventions

- **Git** : branches `main` (prod) / `feature/*` — conventional commits (`feat:`, `fix:`, `chore:`, `docs:`)
- **Secrets** : Ansible Vault (Ansible), SOPS+age (Talos), Sealed Secrets (k8s)
- **Namespaces k8s** : `argocd`, `infra`, `monitoring`, `ai`, `auth`, `external` (+ `sacrifice`, hors-repo)
- **Hermes data** : profils/données dans `/srv/data/hermes` (NVMe, bind-mount `/home/data`), code dans `/opt/hermes/` (SSD OS)
- **RAM compute-02/03** : toujours définir `resources.requests` + `resources.limits` (8 GB RAM, OOM-killer actif)
