docs(runbook): architecture stub forwarder Bifrost VPS, rollback 5s, piege allow_all_keys et injection session opencode (tickets 010 et 008)
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
# Architecture Stub Forwarder Bifrost (NAS → VPS) & Injection Sessions OpenCode (04/09/2026)
|
||||
|
||||
## 1. Objectif & Contexte
|
||||
|
||||
Dans le cadre du ticket `infra-2026-09-010`, la passerelle LLM principale **Bifrost** a été migrée du NAS Synology vers le VPS Contabo (`100.94.90.119`).
|
||||
Pour préserver l'accès LLM des 7+ consommateurs résidant sur le NAS (`context-hub`, `redaction-pro`, `gsparc-mezzouna-api`, `nyora-convert-api`, `nyora-notes-tt`, `reglement-mcp`, et `bifrost-proxy:3086`) **sans modifier aucune de leurs configurations individuelles**, une architecture par **stub forwarder transparent** a été mise en place sur le NAS.
|
||||
|
||||
Parallèlement, pour répondre au ticket `infra-2026-09-008`, le proxy frontal du NAS (`bifrost-proxy`) a été instrumenté pour injecter automatiquement l'en-tête de session requis par OpenCode Go (`x-opencode-session`), transmis de manière transparente par Bifrost.
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture Technique
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph NAS ["NAS Synology (Zone Sud)"]
|
||||
Hermes["Hermes Agents"] --> Proxy["bifrost-proxy:3086 (Nginx)"]
|
||||
Proxy -- "x-bf-eh-x-opencode-session" --> Stub
|
||||
DirectClients["context-hub / redaction-pro / etc."] --> Stub["bifrost (stub socat:3085)"]
|
||||
Stub --> Bridge["tailscale-nyora-bridge:1055 (SOCKS5)"]
|
||||
end
|
||||
|
||||
subgraph VPS ["VPS Contabo (100.94.90.119)"]
|
||||
Bridge -- "Tunnel Tailscale" --> BifrostVPS["bifrost:3085 (maximhq/bifrost)"]
|
||||
BifrostVPS --> Providers["Providers Externes (Google, OpenCode Go, Anthropic)"]
|
||||
end
|
||||
```
|
||||
|
||||
### Le stub socat sur le NAS
|
||||
- **Emplacement** : `/volume1/docker/bifrost-stub/docker-compose.yml`
|
||||
- **Nom du conteneur** : `bifrost`
|
||||
- **Réseau Docker** : `n8n` (external)
|
||||
- **Ports exposés** : `"3085:8080"`
|
||||
- **Commande** :
|
||||
```yaml
|
||||
image: alpine/socat:latest
|
||||
container_name: bifrost
|
||||
restart: unless-stopped
|
||||
command: ["-d", "-d", "TCP-LISTEN:8080,fork,reuseaddr", "SOCKS5-CONNECT:tailscale-nyora-bridge:100.94.90.119:3085,socksport=1055"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Procédure de Rollback d'Urgence (< 5 secondes)
|
||||
|
||||
L'ancien conteneur Bifrost local du NAS n'a **jamais été supprimé**. Il a été simplement renommé et stoppé.
|
||||
|
||||
En cas de coupure prolongée du lien Tailscale ou d'anomalie sur le VPS, exécuter sur le NAS :
|
||||
```bash
|
||||
# 1. Arrêter et écarter le stub socat
|
||||
docker stop bifrost
|
||||
docker rename bifrost bifrost-stub-temp
|
||||
|
||||
# 2. Restaurer le conteneur NAS natif
|
||||
docker rename bifrost-nas-backup bifrost
|
||||
docker start bifrost
|
||||
|
||||
# 3. Réactualiser la résolution DNS du proxy frontal
|
||||
docker restart bifrost-proxy
|
||||
```
|
||||
|
||||
*Le rétablissement du service local est immédiat (aucune réimportation de base nécessaire).*
|
||||
|
||||
---
|
||||
|
||||
## 4. Le Piège `allow_all_keys` et `config.json` au Boot
|
||||
|
||||
### La cause racine
|
||||
Bifrost (version `maximhq/bifrost:v1.6.11`) possède un comportement spécifique : au redémarrage, il recharge sa configuration depuis `config.json`. Si une clé virtuelle n'est pas explicitement autorisée pour chaque fournisseur dans la base SQLite interne, Bifrost bloque les requêtes avec une erreur HTTP 401 (`key not allowed for provider`).
|
||||
|
||||
### La vraie table SQLite
|
||||
Contrairement à d'anciennes mentions erronées pointant `config_providers`, la table concernée est :
|
||||
- **Table** : `governance_virtual_key_provider_configs`
|
||||
- **Colonne** : `allow_all_keys` (type numeric, booléen)
|
||||
- **Nombre d'entrées** : 81 configurations actives (combinaisons clés virtuelles / providers)
|
||||
|
||||
### Vérification de l'intégrité (VPS ou NAS)
|
||||
```bash
|
||||
python3 -c '
|
||||
import sqlite3
|
||||
c = sqlite3.connect("/home/dsh-agent/bifrost/data/config.db").cursor()
|
||||
c.execute("SELECT count(*), sum(allow_all_keys) FROM governance_virtual_key_provider_configs;")
|
||||
count, active = c.fetchone()
|
||||
print(f"Configurations conformes : {active}/{count}")
|
||||
'
|
||||
```
|
||||
*Attendu : `(81, 81)`.*
|
||||
|
||||
### Rattrapage SQL si redéploiement d'une base neuve
|
||||
Si une nouvelle instance Bifrost est provisionnée :
|
||||
```sql
|
||||
UPDATE governance_virtual_key_provider_configs SET allow_all_keys = 1 WHERE allow_all_keys != 1;
|
||||
```
|
||||
Puis redémarrer le conteneur : `docker restart bifrost`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Injection de Session OpenCode Go (`x-bf-eh-x-opencode-session`)
|
||||
|
||||
### Problème résolu (Ticket infra-2026-09-008)
|
||||
À partir de septembre 2026, l'API OpenCode Go exige la présence d'un en-tête `x-opencode-session`.
|
||||
Une tentative de sidecar local sur le VPS échoue car le validateur SSRF interne de Bifrost (`net.IP.IsPrivate()`) interdit toute passerelle locale (`172.25.0.2` ou `127.0.0.1`).
|
||||
|
||||
### Solution native Bifrost
|
||||
Bifrost supporte nativement le relais d'en-têtes HTTP arbitraires vers les fournisseurs amont via le préfixe `x-bf-eh-*`. Tout en-tête débutant par `x-bf-eh-` est débarrassé de son préfixe par Bifrost et transmis au provider distant en HTTPS public.
|
||||
|
||||
### Implémentation dans `bifrost-proxy` (`/volume1/docker/bifrost-proxy/nginx.conf`)
|
||||
```nginx
|
||||
http {
|
||||
...
|
||||
map $http_x_opencode_session $opencode_session {
|
||||
default $http_x_opencode_session;
|
||||
"" $resolved_vk;
|
||||
}
|
||||
|
||||
server {
|
||||
...
|
||||
location / {
|
||||
...
|
||||
proxy_pass http://bifrost:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header x-bf-vk $resolved_vk;
|
||||
proxy_set_header x-bf-eh-x-opencode-session $opencode_session;
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- Si le client fournit déjà `x-opencode-session`, il est conservé.
|
||||
- Sinon, la clé virtuelle résolue (`$resolved_vk`) sert d'identifiant de session stable.
|
||||
- Les autres fournisseurs (Google, Anthropic) ignorent silencieusement cet en-tête supplémentaire sans perturbation.
|
||||
|
||||
---
|
||||
|
||||
## 6. Vérifications et Tests Rapides
|
||||
|
||||
### 1. Test de connectivité du stub depuis un conteneur NAS
|
||||
```bash
|
||||
docker exec context-hub python3 -c '
|
||||
import urllib.request
|
||||
with urllib.request.urlopen("http://bifrost:8080/health", timeout=5) as r:
|
||||
print("Statut stub NAS -> VPS :", r.status)
|
||||
'
|
||||
```
|
||||
|
||||
### 2. Test d'inférence complète avec injection de session via `bifrost-proxy`
|
||||
```bash
|
||||
curl -s -X POST http://127.0.0.1:3086/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer <VIRTUAL_KEY>" \
|
||||
-d '{"model":"mimo-v2.5","messages":[{"role":"user","content":"ping"}],"max_tokens":10}'
|
||||
```
|
||||
Reference in New Issue
Block a user