4.6 KiB
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\rightarrowbudget TTvk-hermes-nyora\rightarrowbudget Nyoravk-hermes-perso\rightarrowbudget PersoNabil-Key\rightarrowbudget 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
- 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. - 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) :
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 parsk-bf-90d6...vk-hermes-perso: commence parsk-bf-bb06...vk-hermes-tt: commence parsk-bf-2669...Nabil-Key: commence parbfk-0cd1...(à proscrire dans les conteneurs d'agents)
Étape 2 : Auditer config.yaml dans chaque instance Hermes
Sur le NAS Synology :
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
docker exec <nom_conteneur> cp /opt/data/config.yaml /opt/data/config.yaml.bak-$(date +%Y%m%d)-cle-perso
2. Remplacement chirurgical de la valeur
# Exemple pour hermes-agent-perso avec sa clé vk-hermes-perso
docker exec <nom_conteneur> sed -i 's/bfk-0cd1fba7d440ca2eefd30b28a7349b7d28422dcbc49e982d/<VRAIE_CLE_VK>/g' /opt/data/config.yaml
3. Contrôle du diff
docker exec <nom_conteneur> 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
docker restart <nom_conteneur>
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 :
docker exec <nom_conteneur> 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 <latence> ms.