# 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-10)** : - ✅ Hermes Agent opérationnel — multi-profils (funk-ai/system/monitor/brain) - ⚠️ RAG **HS depuis 2026-06-05** — Qdrant en crash-loop (segment `funk-docs` corrompu, voir `admin/incidents.md`) ; `rag-ingest` / `rag-query` indisponibles - ✅ 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) + CPU (:1236/:1237) opérationnels, kernel 5.14.0-687.5.3 - ✅ 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) - ✅ hermes-auto-improve — analyse quotidienne de `admin/` par Hermes à 22h (n8n → trigger :9095), PR sur `hermes/daily-work` - ⏳ 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.7 | Passerelle + bastion admin + données + Hermes Agent — **hors cluster** | | compute-01 | 192.168.10.11 | Talos v1.13 | k8s control-plane | | compute-02 | 192.168.10.12 | Talos v1.13 | k8s worker | | compute-03 | 192.168.10.13 | Talos v1.13 | k8s worker | | gpu-01 | 192.168.10.20 | AlmaLinux 9.7 | llama-server dédié (RX 6700XT, ROCm 7.x) — **hors cluster** | **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) ### Rôles — gpu-01 `hermes_user`, `common`, `rocm`, `nfs_client`, `llama_server`, `node_exporter` > 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//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_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` --- ## 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 │ ├── monitoring.yaml │ └── nfs-provisioner.yaml ├── apps/ # Manifests raw k8s par application │ ├── n8n/ # namespace: ai │ ├── open-webui/ # namespace: ai │ └── {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/ ``` ### Ajouter un nouveau workload k8s 1. Créer `k8s/apps//` avec les manifests (Deployment, Service, Ingress, PVC…) 2. Créer `k8s/apps-of-apps/apps/.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), `monitoring` (Prometheus/Grafana), `ai` (n8n, open-webui). --- ## 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 : — erreurs ?" ``` ### RAG — Documentation Funk > ⚠️ **HS depuis 2026-06-05** — Qdrant en crash-loop (segment `funk-docs` corrompu). Réparation : supprimer la collection corrompue puis relancer `rag-ingest` (reconstructible depuis `admin/`). Qdrant collection `funk-docs` indexée depuis `admin/` (339 chunks, score min 0.60). ```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`, `external` - **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)