# Piège Clé Virtuelle Nabil-Key vs Clé Dédiée Consommateur Bifrost (04/09/2026) ## 1. Contexte & Problématique Chaque instance Hermes et consommateur de la passerelle **Bifrost** dispose d'une clé virtuelle dédiée (Virtual Key ou VK) associée à une politique budgétaire propre dans la table `governance_budgets` : - `vk-hermes-tt` $\rightarrow$ budget TT - `vk-hermes-nyora` $\rightarrow$ budget Nyora - `vk-hermes-perso` $\rightarrow$ budget Perso - `Nabil-Key` $\rightarrow$ budget personnel Nabil (plafond strict : 8 \$/mois) ### Le Piège Lors de configurations initiales ou de migrations manuelles, la valeur secrète de **`Nabil-Key`** (`bfk-0cd1fba7d440ca2eefd30b28a7349b7d28422dcbc49e982d`) a été copiée par erreur dans `/opt/data/config.yaml` d'instances Hermes (`hermes-nyora`, `hermes-perso`). ### Symptômes 1. **Facturation croisée** : L'ensemble des coûts de tokens (appels LLM principaux et/ou appels `auxiliary.vision` / `auxiliary.approval`) est débité du budget personnel de 8 \$/mois au lieu de la sphère appropriée. 2. **Risque de coupure inopinée** : Lorsque le budget de 8 \$ est atteint, Bifrost rejette toutes les requêtes portant cette clé avec une erreur HTTP 429 / 401 (`budget exhausted`), bloquant à la fois l'usage personnel et l'instance Hermes concernée. --- ## 2. Procédure de Diagnostic ### Étape 1 : Récupérer les clés virtuelles officielles depuis Bifrost Sur l'hôte hébergeant Bifrost (actuellement VPS Contabo `100.94.90.119` ou snapshot de base) : ```bash python3 -c " import sqlite3 conn = sqlite3.connect('/home/dsh-agent/bifrost/data/config.db') c = conn.cursor() c.execute('SELECT name, value, is_active FROM governance_virtual_keys;') for name, val, active in c.fetchall(): print(f'{name}: {val[:12]}... (actif={active})') " ``` *Correspondances attendues :* - `vk-hermes-nyora` : commence par `sk-bf-90d6...` - `vk-hermes-perso` : commence par `sk-bf-bb06...` - `vk-hermes-tt` : commence par `sk-bf-2669...` - `Nabil-Key` : commence par `bfk-0cd1...` (à proscrire dans les conteneurs d'agents) ### Étape 2 : Auditer `config.yaml` dans chaque instance Hermes Sur le NAS Synology : ```bash for inst in hermes-agent-tt hermes-agent-nyora hermes-agent-perso; do echo "=== $inst ===" docker exec $inst python3 -c ' import yaml cfg = yaml.safe_load(open("/opt/data/config.yaml")) m_key = cfg.get("model", {}).get("api_key", "") print(" model.api_key:", m_key[:12] if m_key else "vide") for k, v in cfg.get("auxiliary", {}).items(): if isinstance(v, dict) and v.get("api_key"): print(f" auxiliary.{k}.api_key:", v["api_key"][:12]) ' done ``` **Alerte immédiate** si une clé commence par `bfk-0cd1...`. --- ## 3. Procédure de Correction Chirurgicale Ne jamais réécrire l'intégralité du fichier `config.yaml` (risque d'écraser des réglages runtime ou de corrompre l'indentation YAML). Utiliser un remplacement chirurgical par `sed`. ### 1. Sauvegarde préalable obligatoire ```bash docker exec cp /opt/data/config.yaml /opt/data/config.yaml.bak-$(date +%Y%m%d)-cle-perso ``` ### 2. Remplacement chirurgical de la valeur ```bash # Exemple pour hermes-agent-perso avec sa clé vk-hermes-perso docker exec sed -i 's/bfk-0cd1fba7d440ca2eefd30b28a7349b7d28422dcbc49e982d//g' /opt/data/config.yaml ``` ### 3. Contrôle du diff ```bash docker exec diff -u /opt/data/config.yaml.bak-$(date +%Y%m%d)-cle-perso /opt/data/config.yaml ``` *Le diff ne doit comporter strictement que les lignes d'affectation de clé modifiées.* ### 4. Redémarrage propre du conteneur ```bash docker restart ``` --- ## 4. Validation Post-Correction par Inférence Réelle Exécuter un appel réel depuis l'intérieur du conteneur pour valider le bon fonctionnement de bout en bout : ```bash docker exec python3 -c ' import yaml, urllib.request, json, time cfg = yaml.safe_load(open("/opt/data/config.yaml")) m = cfg.get("model", {}) base_url = m.get("base_url", "http://bifrost-proxy/v1").rstrip("/") api_key = m.get("api_key", "") url = base_url + "/chat/completions" payload = { "model": m.get("default", "mimo-v2.5"), "messages": [{"role": "user", "content": "ping"}], "max_tokens": 10 } req = urllib.request.Request( url, headers={ "Content-Type": "application/json", "Authorization": f"Bearer {api_key}" }, data=json.dumps(payload).encode() ) with urllib.request.urlopen(req, timeout=15) as resp: data = json.loads(resp.read().decode()) print("Inférence OK :", data.get("model"), "en", data.get("extra_fields", {}).get("latency"), "ms") ' ``` *Attendu : `Inférence OK : mimo-v2.5 en ms`.*