# 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 " \ -d '{"model":"mimo-v2.5","messages":[{"role":"user","content":"ping"}],"max_tokens":10}' ```