Files
nas-runbooks/common/migration-vps-bifrost-stub-forwarder-20260904.md

6.1 KiB

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

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 :
    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 :

# 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)

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 :

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)

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

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

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}'