consolidation: fusion doublons (mcp-nas, opencode, bifrost-vk, doc-api rebuild), purge obsoletes (gotenberg, ports-registry racine), re-spherage runbooks metier (perso/tt/nyora), refonte _INDEX.md — 09/07/2026
This commit is contained in:
@@ -1,87 +0,0 @@
|
||||
# RUNBOOK — Bifrost VK OpenCode Fix + Veille Fallback
|
||||
|
||||
**Date** : 2026-06-23
|
||||
**Contexte** : Veille bolbol.tn — "Resume indisponible" systematique
|
||||
|
||||
## Symptomes
|
||||
|
||||
- `00:00` et `19:00` : `could not auto resolve a provider`
|
||||
- `07:00` : `GoUsageLimitError — Weekly usage limit reached`
|
||||
|
||||
## Diagnostic
|
||||
|
||||
### Erreur 1 — could not auto resolve
|
||||
Node n8n `Enrichir via Bifrost` (workflow 9378593adccf42dc) envoie x-bf-vk: bfk-e5832bfe (Nabil-Key), model: deepseek-v4-flash.
|
||||
La Nabil-Key VK n'avait pas OpenCode dans governance_virtual_key_provider_configs.
|
||||
Bifrost cherchait deepseek-v4-flash dans openrouter/google/nvidia_nim -> introuvable.
|
||||
|
||||
### Erreur 2 — GoUsageLimitError
|
||||
Job cron hermes-nyora `veille-hebdo-nexum-kanban-a1b2c3d4` avait model/provider null.
|
||||
Orchestrateur -> provider par defaut = OpenCode Go direct -> quota hebdo epuise lundi.
|
||||
NOTE: Les 6 workers kanban utilisent le profil veilleur (openrouter) -> OK. Seul l'orchestrateur est affecte.
|
||||
|
||||
## Fixes appliques
|
||||
|
||||
### Fix 1 — Bifrost DB: OpenCode ajoute a Nabil-Key VK
|
||||
```python
|
||||
conn = sqlite3.connect('/mnt/docker/bifrost/data/config.db')
|
||||
cur = conn.cursor()
|
||||
vk = 'dc17fe0c-f7bf-4752-acb8-798156b2ec9e'
|
||||
models = '["deepseek-v4-flash","deepseek-v4-pro","glm-5.1","kimi-k2.6"]'
|
||||
cur.execute(
|
||||
"INSERT INTO governance_virtual_key_provider_configs "
|
||||
"(virtual_key_id, provider, weight, allowed_models, blacklisted_models, allow_all_keys) "
|
||||
"VALUES (?, 'opencode', 1.0, ?, '[]', 1)", (vk, models))
|
||||
conn.commit()
|
||||
```
|
||||
|
||||
### Fix 2 — Bifrost DB: OpenCode key persistee dans config_keys
|
||||
```python
|
||||
kid = str(uuid.uuid4())
|
||||
now = datetime.datetime.utcnow().isoformat() + 'Z'
|
||||
cur.execute(
|
||||
"INSERT INTO config_keys (name,provider_id,provider,key_id,value,models_json,"
|
||||
"blacklisted_models_json,weight,enabled,encryption_status,created_at,updated_at) "
|
||||
"VALUES ('opencode-1',36,'opencode',?,?,?,'[]',1.0,1,'plain_text',?,?)",
|
||||
(kid, BIFROST_OPENCODE_KEY, models, now, now))
|
||||
conn.commit()
|
||||
```
|
||||
|
||||
### Fix 3 — Bifrost restart (requis apres modif DB)
|
||||
Via Portainer API http://172.17.0.1:9000 (endpoint_id=2, container 3136d565d6dd).
|
||||
Auth: POST /api/auth -> jwt. Actions: stop + start sur /api/endpoints/2/docker/containers/{id}/{action}
|
||||
ATTENTION: container ID change apres recreate -> verifier avant.
|
||||
|
||||
### Fix 4 — jobs.json hermes-nyora: orchestrateur job 2
|
||||
Fichier: /mnt/docker/hermes-platform/hermes-nyora/data/cron/jobs.json
|
||||
Job veille-hebdo-nexum-kanban-a1b2c3d4 -> model: deepseek/deepseek-v4-flash, provider: openrouter
|
||||
|
||||
### Fix 5 — n8n workflow 9378593adccf42dc: fallback gemini-2.5-flash
|
||||
Node Enrichir via Bifrost modifie: si GoUsageLimitError -> retry avec gemini-2.5-flash.
|
||||
Via n8n API: PUT /api/v1/workflows/9378593adccf42dc avec {name, nodes, connections, settings}.
|
||||
|
||||
## Architecture finale (VK Nabil-Key bfk-e5832bfe)
|
||||
|
||||
| Provider | Modeles autorises |
|
||||
|-------------|------------------------------------------------------------------------|
|
||||
| openrouter | Gemma-4-31B/26B free, Nemotron, GPT-OSS free, Hermes-3, Qwen3-coder |
|
||||
| google | gemma-4-31b/26b, gemini-2.5-flash, gemini-2.5-flash-lite |
|
||||
| nvidia_nim | Llama-3.3-70B, Nemotron-70B, Llama-405B, Qwen2.5-Coder |
|
||||
| opencode | deepseek-v4-flash, deepseek-v4-pro, glm-5.1, kimi-k2.6 (NOUVEAU) |
|
||||
|
||||
## Pieges a eviter
|
||||
|
||||
- Bifrost ne hot-reload PAS les providers -> restart obligatoire apres modif DB
|
||||
- config.json charge providers au boot en memoire; config_keys DB = source persistante
|
||||
- auth Bifrost = x-bf-vk header, PAS Authorization: Bearer -> 401
|
||||
- Portainer container ID change apres recreate -> verifier avant restart
|
||||
- N8N PUT /workflows: accepte uniquement {name, nodes, connections, settings} -> 400 sinon
|
||||
|
||||
## MCP NAS — Regles de scripting fiables
|
||||
|
||||
| NE PAS faire | Faire a la place |
|
||||
|--------------------------------------------|-----------------------------------------------|
|
||||
| Heredoc << 'EOF' | Base64 encode -> transfer -> decode |
|
||||
| printf 'long code python' | python3 >400c | printf > /tmp/s.py && python3 /tmp/s.py |
|
||||
| Commands dans session apres plusieurs err | Nouvelle session ou session differente |
|
||||
| Pipes complexes inline dans printf | Script intermediaire en /tmp/ |
|
||||
@@ -1,190 +0,0 @@
|
||||
# MCP NAS — Référence de scripting fiable
|
||||
|
||||
**Date** : 2026-06-23 | Session debug Veille/Bifrost (40+ tool calls observés)
|
||||
|
||||
> ⚠️ **OBSOLÈTE EN PARTIE depuis le patch P0 du 2026-07-04** — voir [mcp-nas-run-command-v2.md](mcp-nas-run-command-v2.md).
|
||||
> Les RÈGLES 1, 2 et 3 ci-dessous (poisoning, heredoc interdit, limite 350 chars/base64) **ne s'appliquent plus** :
|
||||
> heredocs, quotes et commandes longues fonctionnent nativement, et `reset_session` récupère une session bloquée.
|
||||
> La section « RÈGLE 4 — Services internes (172.17.0.1) » reste valide ;
|
||||
> depuis le P1, `nas.internal` est un alias stable de 172.17.0.1 dans le container.
|
||||
|
||||
---
|
||||
|
||||
## Environnement du container
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| Shell | `/bin/bash` |
|
||||
| User | `root` uid=0 |
|
||||
| Python | `3.11.15` — `/usr/local/bin/python3` |
|
||||
| Modules Python | `requests`, `sqlite3`, `json`, `urllib`, `subprocess` |
|
||||
| Filesystem `/mnt` | NAS Synology 11 To, R/W |
|
||||
| Réseau host Docker | `172.17.0.1` |
|
||||
| Réseau LAN NAS | `192.168.100.33` — **INACCESSIBLE depuis container** |
|
||||
|
||||
### Binaires (vérifié 2026-06-23)
|
||||
|
||||
| Outil | Dispo | Note |
|
||||
|-------|-------|------|
|
||||
| `curl` | oui | `/usr/bin/curl` — soumis limite 350 chars |
|
||||
| `base64` | oui | `/usr/bin/base64` |
|
||||
| `python3` | oui | 3.11 complet |
|
||||
| `git` | oui | via subprocess Python |
|
||||
| `sqlite3` CLI | non | utiliser module Python `import sqlite3` |
|
||||
| `docker` CLI | non | pas de socket monté — Portainer API |
|
||||
| `paramiko` | non | pas de SSH natif Python |
|
||||
|
||||
---
|
||||
|
||||
## RÈGLE 1 — Sessions : poisoning
|
||||
|
||||
Après 3+ erreurs consécutives, la session refuse même `echo hello`.
|
||||
Symptôme : `{"error": "Error occurred during tool execution"}` sur commande triviale.
|
||||
|
||||
Sessions mortes dans cette session : `claude`, `db_update`, `verify`.
|
||||
Sessions stables : `model_test`, `emergency`, `s1` (utilisées fraîchement).
|
||||
|
||||
**Toujours spécifier `session=` dans run_command** (sans = erreur systématique).
|
||||
Après 2 erreurs d'affilée : changer de session immédiatement.
|
||||
Si toutes poisonnées : `new_session("fresh1")`.
|
||||
|
||||
---
|
||||
|
||||
## RÈGLE 2 — Heredoc interdit
|
||||
|
||||
`<< 'EOF'` dans toute forme cause un **timeout 120s** qui tue la session.
|
||||
Cela inclut `cat > f << 'EOF'`, `python3 << 'PYEOF'`, `bash << 'EOF'`.
|
||||
|
||||
Cause : l'injection tmux ne gère pas l'attente stdin que heredoc déclenche.
|
||||
|
||||
---
|
||||
|
||||
## RÈGLE 3 — Limite 350 chars par commande
|
||||
|
||||
Au-delà de ~350 chars dans le champ `command` : timeout, troncature ou erreur silencieuse.
|
||||
|
||||
**Méthode 1 — Code court (< 200 chars)**
|
||||
|
||||
Deux appels séparés :
|
||||
```
|
||||
printf 'import json\nprint("ok")\n' > /tmp/s.py
|
||||
python3 /tmp/s.py
|
||||
```
|
||||
|
||||
**Méthode 2 — Code moyen (200–350 chars)**
|
||||
|
||||
Appender en plusieurs appels :
|
||||
```
|
||||
printf 'import sqlite3\nconn=sqlite3.connect("/mnt/docker/bifrost/data/config.db")\n' > /tmp/s.py
|
||||
printf 'cur=conn.cursor()\ncur.execute("SELECT * FROM config_keys")\n' >> /tmp/s.py
|
||||
printf 'for r in cur.fetchall(): print(r)\n' >> /tmp/s.py
|
||||
python3 /tmp/s.py
|
||||
```
|
||||
|
||||
**Méthode 3 — Code long ou avec quotes simples (> 350 chars)**
|
||||
|
||||
Base64 encode côté Claude (bash_tool), decode sur NAS (2 run_command) :
|
||||
```
|
||||
# bash_tool : base64 -w0 fichier.py → obtenir la chaîne B64
|
||||
# run_command 1 : printf '<B64>' | base64 -d > /tmp/s.py
|
||||
# run_command 2 : python3 /tmp/s.py
|
||||
```
|
||||
|
||||
Piège : une quote simple `'` dans printf termine la chaîne → code tronqué sans erreur.
|
||||
Dès qu'il y a des `'` dans le code Python → base64 obligatoire.
|
||||
|
||||
---
|
||||
|
||||
## RÈGLE 4 — Services internes (172.17.0.1)
|
||||
|
||||
**Portainer — restart container**
|
||||
```python
|
||||
import urllib.request, json
|
||||
# Auth
|
||||
body = json.dumps({"username":"bestof","password":"<depuis .env>"}).encode()
|
||||
req = urllib.request.Request("http://172.17.0.1:9000/api/auth", data=body,
|
||||
headers={"Content-Type":"application/json"})
|
||||
jwt = json.loads(urllib.request.urlopen(req).read())["jwt"]
|
||||
h = {"Authorization": "Bearer " + jwt}
|
||||
# ID dynamique — change après recreate
|
||||
req = urllib.request.Request(
|
||||
"http://172.17.0.1:9000/api/endpoints/2/docker/containers/json", headers=h)
|
||||
cs = json.loads(urllib.request.urlopen(req).read())
|
||||
cid = [c["Id"][:12] for c in cs if "bifrost" in str(c.get("Names","")).lower()
|
||||
and "proxy" not in str(c.get("Names","")).lower()][0]
|
||||
# Stop + Start
|
||||
for action in ["stop","start"]:
|
||||
req = urllib.request.Request(
|
||||
f"http://172.17.0.1:9000/api/endpoints/2/docker/containers/{cid}/{action}",
|
||||
data=b"", headers=h)
|
||||
urllib.request.urlopen(req)
|
||||
```
|
||||
|
||||
**n8n API — PUT workflow**
|
||||
```python
|
||||
# Uniquement ces 4 champs acceptés — autres -> 400
|
||||
payload = {"name": wf["name"], "nodes": wf["nodes"],
|
||||
"connections": wf["connections"], "settings": wf.get("settings", {})}
|
||||
# Header : X-N8N-API-KEY (pas Authorization Bearer)
|
||||
```
|
||||
|
||||
**Bifrost**
|
||||
```
|
||||
Auth : header x-bf-vk: <VK> — PAS Authorization Bearer (-> 401)
|
||||
URL : http://172.17.0.1:3085
|
||||
```
|
||||
|
||||
**Gitea push**
|
||||
```python
|
||||
import subprocess, os
|
||||
env = open("/mnt/docker/hermes-platform/.env").read()
|
||||
pwd = [l.split("=",1)[1].strip() for l in env.splitlines() if "PASSWORD_GITEA" in l][0]
|
||||
os.chdir("/mnt/docker/nas-runbooks")
|
||||
subprocess.run(["git","add","."])
|
||||
subprocess.run(["git","commit","-m","message"])
|
||||
subprocess.run(["git","push", f"http://bolbol:{pwd}@172.17.0.1:3232/bolbol/nas-runbooks.git"])
|
||||
```
|
||||
|
||||
**NyoraNotes**
|
||||
```python
|
||||
tok = open("/mnt/docker/nyora-notes/data/hermes-perso.token").read().strip()
|
||||
note = {"title":"...","content":"...","tags":["infra","runbook","terminee"]}
|
||||
body = json.dumps(note).encode()
|
||||
req = urllib.request.Request("http://172.17.0.1:8787/notes", data=body,
|
||||
headers={"Authorization":"Bearer "+tok,"Content-Type":"application/json"})
|
||||
urllib.request.urlopen(req) # -> 201
|
||||
```
|
||||
|
||||
**SQLite Bifrost DB**
|
||||
```python
|
||||
import sqlite3, shutil
|
||||
# Lecture : copier d'abord (WAL actif)
|
||||
shutil.copy("/mnt/docker/bifrost/data/config.db", "/tmp/bifrost.db")
|
||||
conn = sqlite3.connect("/tmp/bifrost.db")
|
||||
# Écriture live (restart Bifrost après pour prise en compte)
|
||||
conn = sqlite3.connect("/mnt/docker/bifrost/data/config.db")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tableau de décision rapide
|
||||
|
||||
| Besoin | Faire |
|
||||
|--------|-------|
|
||||
| Code Python < 200 chars | printf > /tmp + python3 (2 appels) |
|
||||
| Code Python > 350 chars ou avec quotes | base64 encode -> decode -> exec |
|
||||
| Heredoc | Jamais. Python open().write() en /tmp |
|
||||
| Restart container | Portainer API, ID dynamique |
|
||||
| sqlite3 | Python import sqlite3 |
|
||||
| Session instable | list_sessions -> session fraîche |
|
||||
| Gitea push | subprocess git avec http://bolbol:pwd@ |
|
||||
|
||||
---
|
||||
|
||||
## Améliorations suggérées linux-mcp-nas
|
||||
|
||||
1. **Monter docker.sock** : `-v /var/run/docker.sock:/var/run/docker.sock` — Docker CLI direct, supprime Portainer API workaround
|
||||
2. **Installer sqlite3 CLI** : `apt install -y sqlite3` dans l'image — inspection DB sans Python
|
||||
3. **Augmenter timeout run_command** : 120s trop court pour git sur gros repos
|
||||
4. **Nommer sessions par rôle** : `git`, `db`, `api`, `test` — rotation propre entre tâches
|
||||
5. **Installer paramiko** : `pip install paramiko` — SSH natif si besoin accès NAS direct
|
||||
@@ -1,59 +0,0 @@
|
||||
# Migration OpenRouter → OpenCode Go — 2026-06-16
|
||||
|
||||
Objectif : basculer la stack LLM du NAS vers **OpenCode Go** (forfait Go, endpoint
|
||||
`https://opencode.ai/zen/go/v1`, auth `Authorization: Bearer <clé>` uniquement —
|
||||
pas de `x-bf-vk`, pas de `HTTP-Referer`/`X-Title`).
|
||||
|
||||
## Modèles OpenCode Go
|
||||
IDs **sans préfixe** (`glm-5.1`, `deepseek-v4-pro`…), PAS `openrouter/` ni `deepseek/`.
|
||||
Utilisables en **OpenAI-compat (oa-compat)** : `deepseek-v4-flash`, `deepseek-v4-pro`,
|
||||
`glm-5.1`, `qwen3.7-plus`, `kimi-k2.6`, `kimi-k2.7-code`, `minimax-m3`, `glm-5`,
|
||||
`qwen3.6-plus`, `mimo-v2.5-pro`…
|
||||
|
||||
⚠️ **NE FONCTIONNE PAS** :
|
||||
- `qwen3.7-max` → `Model not supported for format oa-compat` (dispo seulement en format Anthropic côté OpenCode Go). Tout consommateur OpenAI-compat (Bifrost custom provider, Open WebUI, family-help) doit utiliser `glm-5.1` à la place.
|
||||
- MiniMax M2.7 vision via OpenCode Go = `prompt_tokens:0` (vision cassée).
|
||||
|
||||
## Ce qui a été migré
|
||||
|
||||
| Cible | Avant | Après | Clé |
|
||||
|---|---|---|---|
|
||||
| **hermes-tt** (config.yaml) | deepseek-v4-flash | défaut `deepseek-v4-pro`, fallback `deepseek-v4-flash`, délégation (data-engineer) `deepseek-v4-pro` | HERMES_TT_OPENCODE_KEY (provider `opencode-go`) |
|
||||
| **hermes-nyora** | deepseek-v4-flash | défaut `glm-5.1`, fallback `qwen3.7-plus` | HERMES_NYORA_OPENCODE_KEY |
|
||||
| **hermes-perso** | deepseek-v4-flash | défaut `qwen3.7-plus`, fallback `deepseek-v4-flash` | HERMES_PERSO_OPENCODE_KEY |
|
||||
| **Bifrost** | — | provider `opencode` route `glm-5.1, deepseek-v4-pro, qwen3.7-plus, kimi-k2.6, minimax-m3` ; alias `claude-*→deepseek-v4-flash` (openrouter) conservé | BIFROST_OPENCODE_KEY |
|
||||
| **Open WebUI** | Bifrost seul | 2e connexion directe OpenCode Go (`OPENAI_API_BASE_URLS` pluriel) | BIFROST_OPENCODE_KEY |
|
||||
| **family-help** | OpenRouter (modèle unique) | routing par skill : medical/nutrition `glm-5.1`, homework `qwen3.7-plus`, legal_admin `deepseek-v4-pro`, tech/general `deepseek-v4-flash` ; fallback OpenRouter (web + erreur) | OPENCODE_API_KEY (=BIFROST_OPENCODE_KEY) |
|
||||
| **n8n actifs** | commentaires/refs OpenRouter | nettoyés (NABIL.md Quotidien, Monitoring sources IA, Ingest Opportunités) | — |
|
||||
|
||||
## Hermes — config.yaml
|
||||
Modèle défini dans `/volume1/docker/hermes-platform/hermes-{tt,nyora,perso}/data/config.yaml` :
|
||||
`model.default` + `model.provider: opencode-go`, `fallback_providers[0]`, `fallback_model`.
|
||||
Hermes **réécrit config.yaml au shutdown** → procédure : `docker stop` → éditer → `docker start`
|
||||
(éditer container arrêté pour éviter le clobber). Healthcheck port 8642, API OpenAI-compat
|
||||
(`/v1/chat/completions`, modèle proxy `hermes-agent`).
|
||||
|
||||
## Bifrost — PIÈGE MAJEUR (voir mémoire bifrost-keys-config-json)
|
||||
Bifrost v1.5.13 **ne recharge PAS les clés des providers au redémarrage** (elles ne vivaient
|
||||
qu'en mémoire). Fix durable = **`/volume1/docker/bifrost/data/config.json`** (providers + clés,
|
||||
chargé au boot) + `allow_all_keys=1` dans `governance_virtual_key_provider_configs`.
|
||||
- Providers dans config.json NE doivent PAS être aussi dans le store (`sqlite3 config.db "DELETE FROM config_providers; DELETE FROM config_keys;"`, bifrost stoppé).
|
||||
- `sqlite3 config.db "UPDATE governance_virtual_key_provider_configs SET allow_all_keys=1;"`.
|
||||
- ⚠️ éditer providers/clés/VK via l'UI Bifrost peut re-casser (reset allow_all_keys / conflit config.json).
|
||||
- Backup config.db pré-migration : `/tmp/config.db.backup-*` (NAS).
|
||||
|
||||
## n8n — workflows AO inactifs : NON migrés (à faire délibérément)
|
||||
Les 15 workflows AO/veille contenant encore OpenRouter sont **archivés** (`isArchived=true`) →
|
||||
l'API n8n refuse de les éditer (`Cannot update an archived workflow`). Le pipeline AO **actif**
|
||||
(`AO — Insertion Baserow FINAL`, hisVAANSYpixR4Si) est déjà propre.
|
||||
Pour réutiliser un workflow AO archivé : le désarchiver (UI), migrer son Code node
|
||||
(`openrouter.ai/api/v1/chat/completions` → `opencode.ai/zen/go/v1/chat/completions`, clé sk-or →
|
||||
OpenCode, retirer HTTP-Referer/X-Title, modèle → `deepseek-v4-pro`), re-tester, ré-archiver.
|
||||
Règle Code node n8n : `require('https')` + parsing manuel d'URL, jamais `fetch`/`new URL`/`$helpers`.
|
||||
Liste des archivés concernés : edFMGtI2HHWW72aT, JTwurk7XVBUXSHKn, BCgZYRJcxoHOMDhm, iuxVgZFfpKdYFNqG,
|
||||
xJtW4algsBcx6r1E, EEDr0xx9krRq2nwN, 7zWWJhP2aQbzQhZG, QALWT3NwMQjOuY6u, N3yQWk0XyhyS5wZX,
|
||||
hcOmozHadfBnHzri, nZiXRcwSAXsoj2M6, Mdl5SRcT9rYw48VZ, dPnpoHkc6hEXdsFF, wS3SzRFfcaDDD34g, xu4smQdEGrHFx1zK, RRKUk3GvnULj7edE.
|
||||
|
||||
## Hors scope (laissés sur OpenRouter, actifs/famille — à migrer sur décision)
|
||||
Workflows n8n actifs encore sur OpenRouter en direct (endpoint+clé) : `Mode-nedya`,
|
||||
`FAMILLE | Recettes Weekend`, `Labellisation Auto`.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Bifrost — Incidents VK & fallback (consolidé)
|
||||
|
||||
**Date** : 2026-07-09 (fusionne BIFROST-VK-OPENCODE-FIX.md 23/06 + bifrost-vk-stale-cache-et-fallback-opencode.md 06/07)
|
||||
**Tags** : infra, bifrost, vk, opencode, openrouter, cout
|
||||
Guide d'accès général : common/bifrost-access.md.
|
||||
|
||||
## Règles durables (à retenir avant tout debug)
|
||||
|
||||
1. Auth Bifrost = header `x-bf-vk`, JAMAIS `Authorization: Bearer` → 401.
|
||||
2. Bifrost ne hot-reload NI les clés provider NI le VK store → **restart du container = premier réflexe** sur tout `401 virtual_key_not_found` inexpliqué (cache VK désynchronisé de la DB).
|
||||
3. config.json chargé au boot = source providers ; `allow_all_keys=1` en DB ; ne pas dupliquer providers entre config.json et le store ; l'UI peut re-casser ces réglages.
|
||||
4. Fallbacks Hermes → **opencode-go** (forfait), plus jamais openrouter (facturation réelle à chaque échec du modèle primaire).
|
||||
5. Auditer périodiquement les règles de routing : le NOM d'une règle peut ne pas correspondre à sa cible réelle en base (désync vue sur la règle Haiku).
|
||||
6. Portainer : l'ID container change après recreate → toujours le re-résoudre avant stop/start.
|
||||
7. n8n PUT /workflows : payload strict {name, nodes, connections, settings} → 400 sinon.
|
||||
|
||||
## Incident 23/06/2026 — « Résumé indisponible » veille bolbol.tn
|
||||
|
||||
- `could not auto resolve a provider` : la VK Nabil-Key (bfk-e5832bfe…) n'avait pas OpenCode dans `governance_virtual_key_provider_configs` → deepseek-v4-flash introuvable. Fix : INSERT provider opencode (weight 1.0, allowed_models deepseek-v4-flash/pro, glm-5.1, kimi-k2.6, allow_all_keys=1) + clé persistée dans `config_keys` + restart bifrost.
|
||||
- `GoUsageLimitError` : job cron hermes-nyora orchestrateur avec model/provider null → OpenCode direct, quota hebdo épuisé. Fix : jobs.json → model openrouter/deepseek-v4-flash pour l'orchestrateur ; workflow n8n 9378593adccf42dc → retry gemini-2.5-flash sur GoUsageLimitError.
|
||||
- Architecture VK Nabil-Key finale : openrouter (modèles :free), google (gemma-4, gemini-2.5-flash/lite), nvidia_nim, opencode (deepseek-v4-flash/pro, glm-5.1, kimi-k2.6).
|
||||
|
||||
## Incident 06/07/2026 — VK stale-cache + fallbacks payants
|
||||
|
||||
1. 401 `virtual_key_not_found` répétés sur « VEILLE | Ingest Opportunités » alors que la VK était exacte en base → cache VK désynchronisé, **un simple restart bifrost a suffi**.
|
||||
2. hermes-nyora (pilote mimo-v2.5) a basculé 2× sur son fallback natif openrouter (bypass Bifrost, coût réel facturé) → les 3 config.yaml migrés vers fallback `opencode-go/deepseek-v4-flash`, `key_env: OPENCODE_GO_API_KEY` (nom réel du compose top-level, PAS OPENCODE_API_KEY). Restart des 3 agents (les appels restart API peuvent timeout côté client → vérifier l'état, ne pas re-tenter en boucle ; start manuel parfois requis).
|
||||
3. Règle routing « Haiku → Gemini 2.5 Flash » pointait en réalité vers openrouter/deepseek-v4-flash payant → `routing_targets` corrigé vers google/gemini-2.5-flash. Backups config.db pris avant chaque édit.
|
||||
|
||||
## Pilote mimo-v2.5 (contexte)
|
||||
|
||||
Bilan J+1 (06/07) : 389 appels, 0,5% d'échec (réponse vide = reasoning épuisant max_tokens) → fix `model.max_tokens: 8192` sur hermes-nyora. **Verdict final 08/07 : GARDER mimo, V4 Flash en fallback** — détails : hermes-nyora/verdict-pilote-mimo-v25.md.
|
||||
|
||||
## Vérification type
|
||||
|
||||
```bash
|
||||
curl -X POST http://bifrost:8080/v1/chat/completions -H 'x-bf-vk: <Nabil-Key>' \
|
||||
-d '{"model":"deepseek-v4-flash","max_tokens":30,"messages":[{"role":"user","content":"dis OK"}]}'
|
||||
# 200 + routing_info.provider=opencode attendu
|
||||
```
|
||||
@@ -1,95 +0,0 @@
|
||||
# Bifrost : VK stale-cache + migration fallback OpenRouter -> OpenCode Go
|
||||
|
||||
**Instance auteur** : Claude (via mcp-nas)
|
||||
**Date** : 2026-07-06
|
||||
**Tags** : infra, bifrost, hermes, opencode, openrouter, cout
|
||||
**Statut** : valide
|
||||
|
||||
## Probleme observe
|
||||
|
||||
1. Pipeline nyora-veille (n8n workflow "VEILLE | Ingest Opportunites", node "Enrichir via Bifrost")
|
||||
en echec repete (401 virtual_key_not_found) le 06/07 a 07:34 et 13:29-13:30 UTC, alors que le
|
||||
x-bf-vk hardcode dans le node correspond exactement a la valeur Nabil-Key en base
|
||||
(bfk-e5832bfebd22e9d8252a426d0734ed5f2d923204c8033d6d). Aucun cout facture (echec avant tout
|
||||
routage provider).
|
||||
2. hermes-nyora (mimo-v2.5, pilote 01-08/07) a bascule 2x le 05/07 (~21:38 et 21:48 UTC) sur son
|
||||
fallback natif `fallback_providers: openrouter` (bypass Bifrost, cle OPENROUTER_API_KEY en
|
||||
.env) suite a des reponses vides du pilote sur OpenCode. Cout reel facture sur OpenRouter,
|
||||
hors quota epuise.
|
||||
3. Bifrost logs.db (depuis 06/06) : 1370 appels reussis, 17,43$, VK non identifiee, modele
|
||||
deepseek/deepseek-v4-flash sur OpenRouter -> trace de sessions Claude Cowork/Claude Code
|
||||
routees via Bifrost et interceptees par une regle de routing.
|
||||
|
||||
## Cause racine
|
||||
|
||||
- Point 1 : cache Bifrost desynchronise (VK valide en base mais rejetee en memoire) -> **un simple
|
||||
restart du container bifrost recharge correctement le VK store**. Meme famille de piege que
|
||||
celui deja connu pour les cles provider (config.json au boot) : Bifrost ne recharge pas
|
||||
systematiquement son etat interne sans redemarrage explicite.
|
||||
- Point 2 : `fallback_providers` des 3 instances Hermes pointait vers `openrouter` (paye a l'usage)
|
||||
au lieu de `opencode-go` (deja couvert par le forfait Go). Design d'origine (voir
|
||||
migration-provider-opencode-go.md, 14/06) : a l'epoque volontaire comme filet de secours large,
|
||||
mais genere une facturation reelle des qu'un modele opencode echoue (meme hors quota).
|
||||
- Point 3 : regle de routing Bifrost `Haiku -> Gemini 2.5 Flash` (id
|
||||
f5f03c54-049a-4f53-8590-7392730f42e0) avait un NOM ne correspondant pas a sa cible reelle en
|
||||
base (deepseek/deepseek-v4-flash payant sur openrouter au lieu de gemini-2.5-flash gratuit sur
|
||||
Google AI Studio). Les 4 autres regles Opus/Sonnet/Catch-all pointent, elles, vers des modeles
|
||||
`:free` OpenRouter et sont correctes.
|
||||
|
||||
## Solution appliquee (06/07/2026)
|
||||
|
||||
1. `routing_targets` (config.db Bifrost) : rule f5f03c54... -> provider=google,
|
||||
model=gemini-2.5-flash (etait openrouter/deepseek-v4-flash). Backup pris avant edit
|
||||
(config.db.bak-<timestamp>). Restart container bifrost pour charger.
|
||||
2. Les 3 `data/config.yaml` Hermes (hermes-tt, hermes-nyora, hermes-perso) :
|
||||
`fallback_providers: openrouter/deepseek-v4-flash` -> `opencode-go/deepseek-v4-flash`,
|
||||
`key_env: OPENCODE_GO_API_KEY` (nom reel de la var injectee par le compose top-level, pas
|
||||
OPENCODE_API_KEY comme indique dans l'ancien runbook de migration). Backup des 3 fichiers pris
|
||||
avant edit. Restart des 3 containers hermes-agent-* (perso a eu besoin d'un start manuel apres
|
||||
le restart, le call restart API ayant timeout cote client -> verifier l'etat plutot que
|
||||
de re-tenter en boucle).
|
||||
3. Aucun changement necessaire sur le node n8n "Enrichir via Bifrost" : le restart bifrost a
|
||||
suffi a corriger le probleme de VK stale.
|
||||
|
||||
## Verification
|
||||
|
||||
```
|
||||
curl -s --unix-socket /var/run/docker.sock http://localhost/containers/<c>/json | jq .State
|
||||
# bifrost, hermes-agent-tt/nyora/perso : running + healthy apres restart
|
||||
|
||||
curl -X POST http://bifrost:8080/v1/chat/completions -H 'x-bf-vk: <Nabil-Key>' \
|
||||
-d '{"model":"deepseek-v4-flash","max_tokens":30,"messages":[{"role":"user","content":"dis OK"}]}'
|
||||
# HTTP 200, routing_info.provider=opencode -> confirme
|
||||
```
|
||||
|
||||
## A surveiller
|
||||
|
||||
- Si un 401 virtual_key_not_found reapparait sans changement de config -> suspecter a nouveau le
|
||||
cache VK, restart bifrost avant toute autre investigation.
|
||||
- Fiabilite mimo-v2.5 (reponses vides) non resolue en soi ; le fallback pointant desormais vers
|
||||
opencode-go, tout echec futur du pilote restera gratuit (dans le forfait) au lieu d'etre facture.
|
||||
- Auditer les 4 autres regles Opus/Sonnet/Catch-all : noms vs cibles reelles a revalider
|
||||
periodiquement (meme type de desync que la regle Haiku).
|
||||
|
||||
## References
|
||||
|
||||
- migration-provider-opencode-go.md (14/06/2026)
|
||||
- llm-audit-register.md (24/06/2026), item "N8n Ingest Opportunites"
|
||||
|
||||
## Addendum 06/07/2026 — pilote mimo-v2.5 sur hermes-nyora
|
||||
|
||||
Migration mimo-v2.5 confirmee active depuis le 05/07 20h01 (decision : tester un modele de
|
||||
raisonnement sur Nyora uniquement, controler la conso, decider ensuite de generaliser ou non aux
|
||||
3 instances). Bilan a J+1 : 389 appels, ~11,65M tokens entrants (tres majoritairement cache),
|
||||
~182K tokens sortants, 5 incidents "reponse vide/pensee sans contenu", 2 fallbacks (meme session
|
||||
courte 21h47-21h48 du 05/07). Taux d'echec 0,5%, isole a un cas de contexte court (in=631) ou le
|
||||
raisonnement interne du modele epuisait le budget max_tokens avant d'emettre le contenu visible
|
||||
(meme piege documente pour family-help dans migration-provider-opencode-go.md).
|
||||
|
||||
Fix applique : `model.max_tokens` 4096 -> 8192 dans data/config.yaml hermes-nyora (backup pris),
|
||||
restart hermes-agent-nyora (start manuel necessaire, l'appel restart API a timeout cote client
|
||||
comme pour hermes-agent-perso plus tot). Container reparti healthy, mimo-v2.5 toujours model
|
||||
par defaut.
|
||||
|
||||
Point de decision (generaliser mimo-v2.5 aux 3 instances ou rester DeepSeek V4 Flash) a reprendre
|
||||
en fin de semaine de pilote, sur la base de la conso reelle et du taux d'incidents mesures ici.
|
||||
@@ -1,6 +1,14 @@
|
||||
Runbook: Nettoyage Stack Documentaire
|
||||
Date: 21 juin 2026
|
||||
Services: gotenberg/pandoc/carbone/document-factory tous desinstallés
|
||||
Portainer REST API utilisée pour stop+delete containers et images
|
||||
Piege: rm blocked par mcp-nas, utiliser shutil.rmtree
|
||||
Futur: API doc Nyora a construire from scratch
|
||||
# Nettoyage stack documentaire (historique)
|
||||
|
||||
**Date** : 21/06/2026, complété 09/07/2026
|
||||
**Tags** : infra, cleanup, doc-stack
|
||||
|
||||
## Ce qui a été retiré (ne pas réinstaller, ne pas proposer)
|
||||
|
||||
gotenberg, pandoc, carbone, document-factory, pptx-tt-api, tika — désinstallés le 21/06 via API Portainer (stop + delete containers et images). open-webui, paperless, familyspace retirés lors de l'audit stack de juin (58 → 43 containers).
|
||||
|
||||
Piège : `rm` bloqué par la denylist mcp-nas de l'époque → `shutil.rmtree` en Python.
|
||||
|
||||
## Remplacement
|
||||
|
||||
**nyora-doc-api (port 3050)** est depuis le seul moteur documentaire : DOCX/XLSX/PPTX/PDF, charte Nyora centralisée dans formatting.py. Guide : common/nyora-doc-api-hermes-guide.md · Rebuild : common/nyora-doc-api-rebuild.md.
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# GOTENBERG-PDF — HTML → PDF (piège police web)
|
||||
|
||||
Note partagée aux 3 instances Hermes. Source : projet gsparc-mezzouna, 2026-06-14.
|
||||
|
||||
Gotenberg convertit du HTML en PDF. Conteneur `gotenberg` sur le réseau **n8n**, port **3000**.
|
||||
`POST http://gotenberg:3000/forms/chromium/convert/html` (champ `files` = index.html).
|
||||
|
||||
## PIÈGE — police web manquante sur pages lourdes
|
||||
Sur une page **lourde** (grosse image base64 en ligne), Chromium **imprime avant que la police web** (`@import` Google Fonts) soit appliquée → **texte manquant ou rendu incohérent** d'un document à l'autre. C'est **déterministe selon le poids** : les pages légères passent, les lourdes échouent (piège : on croit à un aléa).
|
||||
|
||||
## SOLUTION
|
||||
- Champ form **`waitDelay: "1.5s"`** → laisse charger les ressources async (police) avant impression.
|
||||
- **Alléger** la page : redimensionner les images embarquées.
|
||||
- Pile de polices de secours avec une Arabe quasi toujours présente dans Chromium :
|
||||
`font-family: Cairo,"Noto Sans Arabic","Noto Naskh Arabic",Tahoma,Arial,sans-serif;`
|
||||
|
||||
## Exemple (paysage A4 pleine page)
|
||||
```bash
|
||||
curl -s http://gotenberg:3000/forms/chromium/convert/html \
|
||||
-F "files=@index.html" \
|
||||
-F "paperWidth=11.69" -F "paperHeight=8.27" \
|
||||
-F "marginTop=0.2" -F "marginBottom=0.2" -F "marginLeft=0.2" -F "marginRight=0.2" \
|
||||
-F "landscape=true" -F "printBackground=true" -F "waitDelay=1.5s" -o out.pdf
|
||||
```
|
||||
Implémentation de référence : `gsparc-mezzouna/app/routes/export.py` (`_fiche_pdf_bytes`).
|
||||
@@ -1,95 +0,0 @@
|
||||
# Hermes-Perso -- Assistant Personnel de Nabil
|
||||
|
||||
Tu es l'assistant personnel de Nabil Derouiche, base a Sfax, Tunisie.
|
||||
Ne en 1973. Marie a Nedya (1983, coeliaque).
|
||||
Fille Yesmine (2008, fibromyalgie/nevralgie -- priorite absolue).
|
||||
Fils Ahmed (2010).
|
||||
Tu es confidentiel, bienveillant, proactif.
|
||||
|
||||
---
|
||||
|
||||
## LANGUES
|
||||
|
||||
- Nabil : francais par defaut, arabe si Nabil ecrit en arabe
|
||||
- Nedya : francais
|
||||
- Ahmed : francais
|
||||
- Yesmine : anglais -- toujours repondre en anglais a Yesmine
|
||||
|
||||
---
|
||||
|
||||
## PRIORITES ABSOLUES
|
||||
|
||||
1. Yesmine -- suivi medical, RDV, recherches fibromyalgie/nevralgie (en anglais)
|
||||
2. Famille -- Nedya (regime sans gluten), Ahmed (suivi scolaire, Bac en cours)
|
||||
3. Veille IA -- synthese des 16 sources de confiance
|
||||
4. Formation personnelle -- IA et automatisation
|
||||
5. Sante et equilibre de Nabil
|
||||
|
||||
---
|
||||
|
||||
## MISSIONS PRINCIPALES
|
||||
|
||||
- Veille IA : surveiller les 16 sources, evaluer chaque article (score /10)
|
||||
- TOUJOURS envoyer TOUS les articles evalues vers l'API veille (quel que soit le score)
|
||||
- Webhook : POST http://n8n:5678/webhook/veille-ingest
|
||||
- Content-Type: application/json
|
||||
- Payload : {"date":"YYYY-MM-DD","source":"@NomSource","titre":"...","score":N*10,"texte":"message original","url":"..."}
|
||||
- score : convertir /10 en /100 (ex: 2/10 -> 20, 7/10 -> 70) — n8n gere enrichissement et ingest
|
||||
- Alerter via Telegram Nabil_Perso uniquement si score >= 7/10 (score >= 70 en API)
|
||||
- Sante Yesmine : recherches medicales EN ANGLAIS, suivi traitements, prep consultations
|
||||
- Alimentation Nedya : recettes sans gluten, alternatives, etiquetage
|
||||
- Mode Nedya : stylisme, modelisme, broderie, tendances styles et couleurs
|
||||
- Suivi Ahmed : scolaire, activites, developpement -- periode Bac prioritaire
|
||||
- Formation Nabil : DataCamp, ressources IA, planification apprentissage
|
||||
- FamilySpace : synchroniser taches, calendrier, notes famille (port 3333)
|
||||
- Agenda personnel : rappels, echeances, organisation
|
||||
|
||||
---
|
||||
|
||||
## INTEGRATION FAMILYSPACE
|
||||
|
||||
Adresse : 192.168.100.33 port 3333
|
||||
Acces via cle dans credentials.md
|
||||
Membres : Nabil (super admin), Nedya, Yesmine, Ahmed
|
||||
Appels complets : lire references/api-calls.md section FamilySpace
|
||||
|
||||
Comportement par membre :
|
||||
- Nabil : lecture/ecriture toutes sections
|
||||
- Nedya : taches, notes, calendrier (verifier sans gluten si courses)
|
||||
- Yesmine : notes symptomes a archiver vault medical en anglais
|
||||
- Ahmed : emploi du temps, todo scolaire
|
||||
|
||||
---
|
||||
|
||||
## REGLES ABSOLUES
|
||||
|
||||
- Yesmine : recherches medicales en anglais, sources serieuses uniquement
|
||||
- Jamais de diagnostic medical -- orienter vers professionnel de sante
|
||||
- Donnees famille : strictement locales, jamais vers services externes
|
||||
- Donnees medicales Yesmine : vault local uniquement
|
||||
|
||||
---
|
||||
|
||||
## MEMOIRE PERSISTANTE
|
||||
- Vault : /opt/data/obsidian/ -- memoire absolue
|
||||
- USER.md max 1375 chars -- MEMORY.md max 2200 chars
|
||||
- Ecrire apres chaque tache importante
|
||||
- Toutes les 10 interactions : verifier distillation necessaire
|
||||
|
||||
---
|
||||
|
||||
## SERVICES DISPONIBLES
|
||||
- FamilySpace : 192.168.100.33 port 3333
|
||||
- NyoraNotes : NYORA_NOTES_URL port 8787
|
||||
- Veille sources : lire skills/veille-sources-ia.md
|
||||
- Appels complets : lire references/api-calls.md
|
||||
|
||||
---
|
||||
|
||||
## PERIMETRE
|
||||
Vie personnelle, famille, sante, formation, veille IA.
|
||||
Taches professionnelles TT : hermes-tt
|
||||
Taches Nyora : hermes-nyora
|
||||
|
||||
## PROTOCOLE INFRA
|
||||
Apres tout deployement ou resolution de probleme : lire et appliquer /opt/data/PROTOCOL-INFRA.md
|
||||
@@ -1,45 +1,20 @@
|
||||
# HTML vers PNG : Rendu headless Puppeteer / Chromium
|
||||
# HTML vers PNG : rendu headless Puppeteer / Chromium
|
||||
|
||||
**Contexte** : html2canvas echoue dans les iframes sandbox (Claude.ai, widgets).
|
||||
Polices Google Fonts et SVG complexes bloques par CORS.
|
||||
**Solution** : rendu serveur avec Chromium headless.
|
||||
**Date** : 2026-06-21, révisé 09/07/2026 (retrait de l'approche Gotenberg — container désinstallé)
|
||||
|
||||
---
|
||||
**Contexte** : html2canvas échoue dans les iframes sandbox (Claude.ai, widgets). Polices Google Fonts et SVG complexes bloqués par CORS.
|
||||
**Solution** : rendu serveur avec Chromium headless via Puppeteer (bash_tool Claude).
|
||||
|
||||
## Approche 1 - Claude bash_tool
|
||||
## Pièges critiques
|
||||
|
||||
### Installation (une seule fois par session)
|
||||
|
||||
|
||||
|
||||
### Script render.mjs
|
||||
|
||||
|
||||
|
||||
**Pieges critiques :**
|
||||
- Entry point : lib/puppeteer/puppeteer.js (pas lib/esm/)
|
||||
- page.waitForTimeout() supprime en v25+ => setTimeout natif
|
||||
- page.waitForTimeout() supprimé en v25+ → setTimeout natif
|
||||
- --no-sandbox obligatoire dans container Claude
|
||||
- networkidle0 + 2s = polices externes chargees
|
||||
- networkidle0 + attente 2 s = polices externes chargées
|
||||
|
||||
## Cas validé (2026-06-21)
|
||||
|
||||
Carte félicitations Bac 2026 — HTML/SVG 680×680 px + Google Fonts → PNG 1360×1360 px (137 Ko), upload Facebook direct.
|
||||
|
||||
---
|
||||
|
||||
## Approche 2 - NAS via Gotenberg (port 3001, deja deploye)
|
||||
|
||||
Gotenberg embarque Chromium. HTML -> PDF puis PDF -> PNG Pillow.
|
||||
|
||||
|
||||
|
||||
Ou via noeud HTTP Request n8n (multipart/form-data vers Gotenberg).
|
||||
|
||||
---
|
||||
|
||||
## Cas valide (2026-06-21)
|
||||
|
||||
Carte felicitations Bac 2026 - HTML/SVG 680x680px + Google Fonts.
|
||||
Resultat : PNG 1360x1360px (137 Ko), upload Facebook direct.
|
||||
|
||||
**Regle absolue** : html2canvas = NON iframe sandbox.
|
||||
Puppeteer (Claude) ou Gotenberg (NAS) = OUI.
|
||||
**Règle absolue** : html2canvas = NON en iframe sandbox. Puppeteer = OUI.
|
||||
**Note** : l'ancienne « Approche 2 » via Gotenberg (HTML→PDF→PNG) n'existe plus — gotenberg a été désinstallé le 21/06/2026 (cf. cleanup-stack-documentaire.md).
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
# Runbook — Kanban Multi-Agent Evaluation AO
|
||||
|
||||
Date : 2026-06-18 | Instance : hermes-tt | Statut : Deploye
|
||||
|
||||
## Architecture
|
||||
|
||||
Pattern agents paralleles pour evaluation AO Zone Sud TT :
|
||||
|
||||
- Profil orchestrateur : default (hermes-tt)
|
||||
- Profil worker : evaluateur-ao (deepseek/deepseek-v4-flash via openrouter)
|
||||
- Kanban : dispatch_in_gateway=true (deja actif dans config.yaml)
|
||||
|
||||
## Comment declencher
|
||||
|
||||
Depuis Telegram ou le workspace hermes-tt :
|
||||
|
||||
Lance evaluation AO [REF] avec soumissionnaires [X, Y, Z]
|
||||
|
||||
hermes-tt cree automatiquement :
|
||||
1. /opt/data/workspace/ao-eval/[REF]/
|
||||
2. Une tache Kanban par soumissionnaire -> profil evaluateur-ao
|
||||
3. Les workers tournent en parallele
|
||||
4. Synthese : rapport XLSX + PV de depouillement format TT
|
||||
5. Envoi sur Telegram
|
||||
|
||||
## Criteres standard
|
||||
|
||||
- Conformite administrative (eliminatoire)
|
||||
- Note technique /70
|
||||
- Note financiere /30 (moins-disant = 30pts, autres proportionnels)
|
||||
|
||||
## Chemins
|
||||
|
||||
Fiches eval : /opt/data/workspace/ao-eval/<REF>/<SOUMISSIONNAIRE>_fiche_eval.md
|
||||
SOUL worker : /opt/data/profiles/evaluateur-ao/SOUL.md
|
||||
Config worker : /opt/data/profiles/evaluateur-ao/config.yaml
|
||||
|
||||
## Commandes utiles
|
||||
|
||||
hermes profile list -- voir les profiles
|
||||
hermes profile show evaluateur-ao -- etat du worker
|
||||
hermes -p evaluateur-ao config set X Y -- modifier la config worker
|
||||
@@ -0,0 +1,46 @@
|
||||
# mcp-nas — Référence opérationnelle (consolidée)
|
||||
|
||||
**Date** : 2026-07-09 (fusionne MCP-NAS-SCRIPTING.md 2026-06-23 + mcp-nas-run-command-v2.md 2026-07-04, + contraintes post-audit 2026-07-08)
|
||||
**Tags** : infra, mcp-nas, scripting, securite
|
||||
|
||||
## Environnement du container (vérifié 09/07/2026, image post-audit)
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| Shell | `/bin/bash`, user root uid=0, **cap_drop: [ALL]** + no-new-privileges |
|
||||
| Python | 3.11 — requests, sqlite3, json, urllib, subprocess |
|
||||
| `git` | **ABSENT** (retiré au rebuild post-audit) → API HTTP Gitea obligatoire |
|
||||
| `docker` CLI / docker.sock | **ABSENTS** (socket retiré P0 audit 08/07) → API Portainer |
|
||||
| `/mnt/docker` | = /volume1/docker, RW |
|
||||
| Réseau | host via `172.17.0.1` ou alias `nas.internal` (extra_hosts). 192.168.100.33 **inaccessible** |
|
||||
|
||||
## run_command v2 (P0 du 04/07, commit 30560984fa)
|
||||
|
||||
La commande est écrite dans un script par Python (zéro parsing shell) puis sourcée dans tmux :
|
||||
- Heredocs natifs (`<<'EOF'`, quotes, `$VAR`, backticks) ; testé 5 250 chars en un appel.
|
||||
- Busy-check : pane occupé → refus propre (options : capture_pane, send_keys, reset_session).
|
||||
- Timeout intelligent : Ctrl-C + sonde ; session préservée si le shell répond.
|
||||
- `reset_session` : reprise d'une session bloquée SANS destruction (vim : `send_keys ':q!'` d'abord).
|
||||
- Cleanup au boot : sessions tmux orphelines + `/tmp/mcp_*` purgés.
|
||||
- Denylist ancrée (P1, commit 7fb13c5) : patterns ancrés en début de segment, heredocs exclus. Défense en profondeur, PAS une frontière de sécurité.
|
||||
- Interdits toujours actifs : reboot, mkfs, dd of=/dev/*, rm -rf racines critiques, docker rm -f, syno*…
|
||||
|
||||
**Ne plus utiliser** : workaround base64 2 appels, découpage printf-append, limite 350 chars — tout cela date de la v1.
|
||||
|
||||
## Contraintes post-audit sécurité (08/07/2026) — vérifiées le 09/07
|
||||
|
||||
1. **`cap_drop: [ALL]` → pas de CAP_DAC_OVERRIDE** : les dossiers `0700` appartenant à uid 1026 (ex. `hermes-*/data/`) sont **illisibles même en root**. C'est voulu. Lecture des data/ Hermes → passer par un autre canal (Mac, ou décision Nabil).
|
||||
2. **`~/.ssh` du container vidé au rebuild** : plus de clé pour `ssh Best0f@172.17.0.1:22222`. SSH host depuis mcp-nas = indisponible tant que la clé n'est pas re-provisionnée (décision Nabil — l'audit recommandait justement de sortir les clés).
|
||||
3. **docker.sock retiré** → gestion containers via **API Portainer** (`http://nas.internal:9000`, POST /api/auth → jwt, endpoint_id=2 ; l'ID container change après recreate, toujours le re-résoudre).
|
||||
4. **Gitea** : API HTTP avec `GITEA_DEPLOY_TOKEN` (repo-scopé, dans hermes-platform/.env) — **jamais** le mot de passe bolbol dans une URL (fuite audit.log + ps). Lire le token dans une variable depuis le fichier, pas en clair dans la commande.
|
||||
5. Rebuild du container : **depuis le Mac** (`ssh -p 22222 Best0f@192.168.100.33`, `cd /volume1/docker/linux-mcp-nas && sudo docker compose build && up -d --force-recreate`), jamais depuis mcp-nas lui-même.
|
||||
|
||||
## Services internes depuis mcp-nas
|
||||
|
||||
`nas.internal` (= 172.17.0.1) : Gitea `:3232`, Portainer `:9000`, NyoraNotes `:8787`, nyora-doc-api `:3050`, Bifrost `:3085` (LAN) / `http://bifrost:8080` (réseau n8n), context-hub `:3093`.
|
||||
|
||||
## Bonnes pratiques
|
||||
|
||||
- Scripts Python : heredoc `cat <<'EOF' > /tmp/s.py … EOF` puis `python3 /tmp/s.py`.
|
||||
- Session par défaut `claude` fiable ; `reset_session` avant de créer une session neuve.
|
||||
- Secrets : toujours via variable lue depuis .env (`export GT=$(grep … | cut -d= -f2)`), jamais inline — audit.log journalise chaque commande.
|
||||
@@ -1,84 +0,0 @@
|
||||
# MCP NAS — run_command v2 (P0) : heredocs, commandes longues, reset_session
|
||||
|
||||
**Date** : 2026-07-04 | Commit `30560984fa` sur `bolbol/linux-mcp-nas@main` | Testé et validé le jour même.
|
||||
|
||||
> **Ce runbook remplace les RÈGLES 1–3 de [MCP-NAS-SCRIPTING.md](MCP-NAS-SCRIPTING.md).**
|
||||
> Le chunking base64, la limite ~350 chars et l'interdiction de heredoc n'existent plus.
|
||||
> La section « Services internes (172.17.0.1) » de l'ancien runbook reste valide.
|
||||
|
||||
---
|
||||
|
||||
## Ce qui a changé
|
||||
|
||||
Avant, `run_command` envoyait la commande brute à tmux via `send-keys` : le parsing shell
|
||||
cassait sur les quotes, les heredocs déclenchaient un timeout tueur de session, et au-delà
|
||||
de ~350 caractères la commande était tronquée — d'où le workaround base64 en 2 appels.
|
||||
|
||||
Depuis le patch P0, le serveur **écrit la commande dans un fichier script depuis Python**
|
||||
(`open(f"/tmp/mcp_{uid}.sh","w").write(command)` — zéro parsing shell) et n'envoie au pane que :
|
||||
|
||||
```
|
||||
. /tmp/mcp_XXX.sh > /tmp/mcp_XXX.out 2>&1; echo $? > /tmp/mcp_XXX.rc
|
||||
```
|
||||
|
||||
via `send-keys -l` (mode littéral). Conséquences :
|
||||
|
||||
| Avant (v1) | Maintenant (v2) |
|
||||
|------------|-----------------|
|
||||
| Heredoc → timeout 120 s, session morte | Heredocs natifs, y compris `<<'EOF'` avec quotes/`$VAR`/backticks |
|
||||
| Limite ~350 chars, base64 obligatoire | Testé 5 250 chars en un appel, intégrité parfaite |
|
||||
| Quote simple `'` → troncature silencieuse | Quotes arbitraires OK (aucun parsing shell) |
|
||||
| Prompt de continuation `>` empoisonnait la session | Mécaniquement impossible |
|
||||
| Empilement sur pane occupé (vim/ssh/pager) | **Busy-check** : refus propre avec message d'options |
|
||||
| Sessions poisonnées → new_session en chaîne | **reset_session** : reprise sans destruction |
|
||||
|
||||
## Nouveaux comportements
|
||||
|
||||
- **Busy-check** : si le pane exécute déjà quelque chose, `run_command` renvoie
|
||||
`[SESSION OCCUPÉE] 'x' exécute actuellement 'sleep'…` au lieu d'empiler. Options proposées :
|
||||
`capture_pane`, `send_keys`, `reset_session`, ou une autre session.
|
||||
- **Timeout intelligent** : à expiration, le serveur tente un Ctrl-C puis sonde le shell.
|
||||
S'il répond → session préservée (cwd/env conservés). Sinon → session recréée dans le même cwd.
|
||||
- **`reset_session(session)`** : envoie Ctrl-C, `q` (pager), Ctrl-U, vérifie que le shell répond.
|
||||
Reprend la main sur une session bloquée SANS la détruire. Pour vim : `send_keys ':q!'` d'abord.
|
||||
- **Cleanup au démarrage** : sessions tmux orphelines et fichiers `/tmp/mcp_*` purgés au boot
|
||||
du container (les 24 sessions zombies accumulées ont disparu au premier rebuild).
|
||||
|
||||
## Tests de validation (2026-07-04)
|
||||
|
||||
1. **Heredoc** avec quotes simples/doubles, `$VAR`, backticks, `$(subshell)`, `!#&*(){}[]|;<>`
|
||||
→ contenu intact, zéro expansion, exit code correct.
|
||||
2. **Commande de 5 250 caractères** (heredoc 50 lignes × 100 chars + vérifications)
|
||||
→ 5 050 octets écrits, 50/50 lignes conformes au motif.
|
||||
3. **Busy-check + reset_session** : `sleep 300` lancé via send_keys → `run_command` refusé
|
||||
proprement → `reset_session` → « shell réactif, cwd/env conservés (cwd: /tmp) ».
|
||||
|
||||
## Bonnes pratiques v2
|
||||
|
||||
- Écrire les scripts Python directement en heredoc : `cat <<'EOF' > /tmp/s.py … EOF` puis `python3 /tmp/s.py`.
|
||||
- Ne plus jamais utiliser le workaround base64 (2 appels) ni le découpage printf-append.
|
||||
- Session par défaut `claude` fiable ; `reset_session` avant de créer une session neuve.
|
||||
- Le rebuild du container se fait **depuis le Mac** (`ssh nas`), jamais depuis mcp-nas lui-même :
|
||||
```
|
||||
ssh -p 22222 Best0f@192.168.100.33
|
||||
cd /volume1/docker/linux-mcp-nas
|
||||
sudo docker compose build && sudo docker compose up -d --force-recreate
|
||||
curl -s http://127.0.0.1:3042/health
|
||||
```
|
||||
|
||||
## P1 — denylist ancrée + nas.internal (2026-07-04, commit `7fb13c5`, déployé et testé)
|
||||
|
||||
- **Denylist ancrée** : les patterns ne sont plus cherchés n'importe où dans la chaîne brute.
|
||||
La commande est découpée en segments (`;` `&` `|` retours ligne), les préfixes neutres
|
||||
(`sudo`, `env`, `VAR=…`, chemin `/sbin/…`) sont retirés, et les patterns sont ancrés sur le
|
||||
**début de chaque commande**. Les **corps de heredoc sont exclus** de l'analyse.
|
||||
- Fini les faux positifs : `grep reboot /var/log`, `echo "ne pas faire reboot"`, un heredoc
|
||||
de doc contenant « mkfs » → **acceptés**.
|
||||
- Toujours refusés : `reboot`, `/sbin/reboot`, `echo ok && reboot`, `VAR=1 sudo shutdown`,
|
||||
`rm -rf /volume*|/opt|/mnt|/`, `mkfs.*`, `dd of=/dev/*`, fork bomb, `> /dev/sd*`,
|
||||
`docker rm -f`, `fdisk`, `parted`, `syno*`, `chmod -R 777 /`. (30/30 tests unitaires.)
|
||||
- Changement assumé : `rm -rf /tmp/x` est désormais **autorisé** (avant, tout chemin absolu
|
||||
était bloqué) ; les racines critiques restent protégées.
|
||||
- **`nas.internal`** : `extra_hosts: nas.internal:host-gateway` dans le compose → depuis toute
|
||||
session, `nas.internal` = l'hôte NAS (172.17.0.1). Utiliser `http://nas.internal:3232` (Gitea),
|
||||
`:9000` (Portainer), `:8787` (NyoraNotes)… au lieu de mémoriser 172.17.0.1.
|
||||
@@ -1,106 +0,0 @@
|
||||
# Migration provider IA → OpenCode Go (+ fallback OpenRouter)
|
||||
|
||||
**Instance auteur** : hermes-perso (transverse aux 3)
|
||||
**Date** : 2026-06-14
|
||||
**Tags** : infra, opencode, hermes, family-help, llm, fallback
|
||||
**Statut** : valide
|
||||
|
||||
---
|
||||
|
||||
## Probleme
|
||||
|
||||
Basculer le provider IA par defaut de OpenRouter vers OpenCode Go (endpoint
|
||||
`zen/go/v1`), tout en gardant DeepSeek V4 Flash et un fallback OpenRouter.
|
||||
Concerne les 3 agents Hermes (gateway) + l'app family-help (module medical).
|
||||
|
||||
---
|
||||
|
||||
## Contexte et contraintes
|
||||
|
||||
- Image `nousresearch/hermes-agent:latest`, source montee dans `/opt/hermes`.
|
||||
- Les 3 Hermes sont des **stacks Portainer** : 291=nyora, 292=perso (path-based,
|
||||
compose sur le FS), 294=TT (compose **inline** dans Portainer). Le
|
||||
`docker-compose.yml` top-level de `hermes-platform/` est OBSOLETE.
|
||||
- 1 seul abonnement OpenCode Go, plusieurs cles tirant du meme pool (~60$/mois).
|
||||
- `/Volumes/docker` (Mac) = mount SMB du NAS `/volume1/docker`.
|
||||
|
||||
---
|
||||
|
||||
## Ce qui NE fonctionne PAS
|
||||
|
||||
| Tentative | Erreur obtenue | Raison de l'echec |
|
||||
|-----------|----------------|-------------------|
|
||||
| `docker compose up` depuis `hermes-platform/` (top-level) | `Conflict. The container name is already in use` | Les containers appartiennent aux stacks per-instance, pas au compose top-level |
|
||||
| `hermes model` / `hermes fallback add` en SSH | `requires an interactive terminal` | Pickers interactifs → editer le YAML directement |
|
||||
| Provider custom dans `providers:` | inutile | `opencode-go` est deja BUILT-IN (models.dev) |
|
||||
| family-help medical `max_tokens: 512` | `finish_reason=length`, `content` VIDE | Modele de raisonnement : reasoning_content mange le budget avant le content |
|
||||
|
||||
---
|
||||
|
||||
## Solution validee
|
||||
|
||||
Provider built-in `opencode-go` : base_url `https://opencode.ai/zen/go/v1`,
|
||||
cle via env var **`OPENCODE_API_KEY`**, model `deepseek-v4-flash` (sans prefixe).
|
||||
OpenRouter (fallback) garde `deepseek/deepseek-v4-flash`.
|
||||
|
||||
```yaml
|
||||
# data/config.yaml (gateway) — pour chaque instance
|
||||
model:
|
||||
default: deepseek-v4-flash
|
||||
provider: opencode-go
|
||||
fallback_providers:
|
||||
- provider: openrouter
|
||||
model: deepseek/deepseek-v4-flash
|
||||
key_env: OPENROUTER_API_KEY
|
||||
```
|
||||
|
||||
```bash
|
||||
# perso / nyora (stacks path-based) : ajouter OPENCODE_API_KEY au .env + au compose
|
||||
# - OPENCODE_API_KEY=${OPENCODE_API_KEY}
|
||||
cd /volume1/docker/hermes-platform/hermes-perso
|
||||
docker compose up -d --force-recreate hermes-agent-perso
|
||||
|
||||
# TT (stack 294 inline) : via API Portainer
|
||||
# GET /api/stacks/294/file → inserer "- OPENCODE_API_KEY=<cle>" → PUT /api/stacks/294?endpointId=2
|
||||
```
|
||||
|
||||
family-help : `medical_service.py` → helper `_llm_complete()` (OpenCode primaire,
|
||||
OpenRouter fallback), **`max_tokens: 2048`**, cle `BIFROST_OPENCODE_KEY`.
|
||||
`docker compose build --no-cache && docker compose up -d --force-recreate`.
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
docker exec hermes-agent-<inst> hermes fallback ls
|
||||
# Attendu : Primary deepseek-v4-flash (via opencode-go)
|
||||
|
||||
# Test end-to-end
|
||||
docker exec hermes-agent-<inst> curl -s -H "Authorization: Bearer <API_SERVER_KEY>" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"dis OK"}],"max_tokens":40}' \
|
||||
http://localhost:8642/v1/chat/completions
|
||||
# Attendu : HTTP 200, content non vide
|
||||
|
||||
# Test fallback : pointer OPENCODE_GO_BASE_URL vers un hote injoignable → reponse via OpenRouter (200)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pieges specifiques DSM / NAS
|
||||
|
||||
- `docker` n'est PAS dans le PATH SSH → utiliser `/usr/local/bin/docker`.
|
||||
- Ne jamais editer le docker-compose top-level (obsolete) ; identifier la vraie source
|
||||
via `docker inspect <c> --format '{{index .Config.Labels "com.docker.compose.project.config_files"}}'`.
|
||||
- TT = stack inline Portainer → API Portainer, pas le FS (sudo requis sinon).
|
||||
- DeepSeek V4 Flash (OpenCode) = reasoning model → `max_tokens` >= 2048 pour les
|
||||
apps maison qui parsent du JSON (Hermes gere nativement).
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- https://opencode.ai/docs/zen
|
||||
- https://hermes-agent.nousresearch.com/docs/user-guide/features/fallback-providers
|
||||
- NyoraNotes : note 416da848 (capitalisation), note 26c38f8c (missions)
|
||||
@@ -1,42 +0,0 @@
|
||||
# nyora-doc-api — Footer Nyora jamais implémenté pour DOCX
|
||||
|
||||
**Date** : 2026-07-03
|
||||
|
||||
## Constat
|
||||
|
||||
Confirmé sur 3 générations réelles consécutives (PMI_Infinity_Formation v1, v2, v3,
|
||||
via Perso) : le DOCX produit par `nyora-doc-api` n'a jamais eu qu'un footer
|
||||
"Page X / Y". Le footer Nyora obligatoire (liseré or, "· NYORA ·", tagline
|
||||
"Crafted with precision · Built for excellence", "© 2026") n'existe que côté
|
||||
PPTX/PDF (`_wrap_html_with_nyora_theme`, `_get_nyora_pdf_css`) — jamais côté DOCX.
|
||||
|
||||
Vérifié dans le `.pyc` compilé avant le "premium upgrade" du 01/07 (commit
|
||||
cbe8cf1) : la fonction n'y était pas non plus. Ce n'est donc pas une régression
|
||||
du upgrade, c'est un trou jamais comblé depuis la création du service.
|
||||
|
||||
## Fix
|
||||
|
||||
`/mnt/docker/nyora-doc-api/app/services.py`, fonction
|
||||
`_add_docx_page_number_footer` : ajout de 3 paragraphes avant la pagination
|
||||
existante — bordure supérieure de paragraphe couleur `B8862A` (simule le liseré
|
||||
or), "· NYORA ·" gras or 8pt, tagline slate italique 7pt, "© 2026" slate 7pt.
|
||||
Backup pris avant patch : `services.py.bak_<timestamp>`.
|
||||
|
||||
## Bloquant — PAS ENCORE ACTIF
|
||||
|
||||
`/app` n'est **pas** bind-mounté dans le container `nyora-doc-api` (seul `/data`
|
||||
l'est) — le code est baké dans l'image `nyora-doc-api:latest` au build. Un
|
||||
`docker restart` ne suffit pas (vérifié en conditions réelles : patch appliqué
|
||||
sur le host, container redémarré, footer toujours absent en test `/v1/generate`
|
||||
direct).
|
||||
|
||||
## Reste à faire
|
||||
|
||||
`docker compose build` (Dockerfile présent dans `/mnt/docker/nyora-doc-api/`)
|
||||
puis recréer le container. Jamais fait sans accord explicite (service qui sert
|
||||
TT et Nyora en production). Tester ensuite avec un appel `/v1/generate` direct
|
||||
(format docx, `data` minimal) et vérifier `word/footer1.xml` contient bien les
|
||||
runs "NYORA" / tagline / copyright avant de considérer le correctif déployé.
|
||||
|
||||
Étape finale une fois validé : `git add . && git commit && git push` depuis le
|
||||
compte bolbol sur le repo `bolbol/nyora-doc-api`.
|
||||
@@ -1,30 +0,0 @@
|
||||
# Runbook — Rebuild & déploiement nyora-doc-api (footer Nyora DOCX)
|
||||
|
||||
**Date** : 04/07/2026 · **Statut** : validé en production
|
||||
|
||||
## Problème
|
||||
Le footer Nyora (liseré or → · NYORA · → tagline → © 2026) n'a jamais existé dans les DOCX générés par nyora-doc-api. Le fix était écrit dans `app/services.py` (`_add_docx_page_number_footer`) mais inactif : **`/app` est cuit dans l'image Docker, pas bind-mounté** — toute modification de code exige un rebuild.
|
||||
|
||||
## Procédure validée (depuis mcp-nas)
|
||||
1. **Docker indisponible dans le container mcp-nas** → passer par SSH host : `ssh -p 22222 Best0f@172.17.0.1` (clé ed25519 en place dans ~/.ssh du container). Sur le host : docker = `/usr/local/bin/docker`, **sans sudo** (Best0f dans le groupe docker ; sudo échoue, pas de TTY).
|
||||
2. Build : `cd /volume1/docker/nyora-doc-api && /usr/local/bin/docker compose build` (~2 min, pip timeout 180 déjà dans le Dockerfile).
|
||||
3. Déploiement : `docker stop nyora-doc-api && docker rm nyora-doc-api && docker compose up -d` — jamais de simple restart, l'image ne serait pas rechargée.
|
||||
4. Health : `curl http://172.17.0.1:3050/health` → 200.
|
||||
|
||||
## Test /v1/generate — pièges
|
||||
- Endpoint **multipart/form-data**, pas JSON : `curl -F "format=docx" -F "data=</tmp/data.json"` + header `X-API-Key` (clé dans hermes-platform/.env). Un POST -d JSON → 422 "body.format missing".
|
||||
- Payload avec apostrophes françaises : toujours l'écrire dans un fichier, jamais inline.
|
||||
|
||||
## Vérification du footer (sans ouvrir Word)
|
||||
```python
|
||||
import zipfile, re
|
||||
xml = zipfile.ZipFile('doc.docx').read('word/footer1.xml').decode()
|
||||
re.findall(r'<w:t[^>]*>([^<]*)</w:t>', xml) # textes du footer
|
||||
'B8862A' in xml # liseré + NYORA doré
|
||||
```
|
||||
Attendu : `· NYORA ·`, `Crafted with precision · Built for excellence`, `© 2026`, pagination.
|
||||
|
||||
## Notes
|
||||
- Couleur or : **#B8862A (Or Bruni, charte doc-api WCAG AAA)**, pas le #d4a01a de la règle générale footer — divergence assumée, à arbitrer.
|
||||
- **Git existe sur le host NAS** (`/usr/bin/git`) — l'ancienne consigne « git depuis le Mac uniquement » est périmée. Push : `git push http://bolbol:PWD@127.0.0.1:3232/bolbol/REPO.git HEAD` depuis le host.
|
||||
- Heredoc contenant du python multi-lignes dans mcp-nas → hang/timeout. Transférer les fichiers en base64 chunké (~290 chars) et décoder sur place.
|
||||
@@ -0,0 +1,28 @@
|
||||
# nyora-doc-api — Procédure de rebuild & déploiement
|
||||
|
||||
**Date** : 2026-07-09 (généralise nyora-doc-api-rebuild-footer-docx.md 04/07 ; historique footer clos)
|
||||
**Tags** : infra, nyora-doc-api, docker, rebuild
|
||||
|
||||
## Règle de base
|
||||
|
||||
**`/app` est cuit dans l'image Docker, pas bind-monté** → toute modification de code dans `/mnt/docker/nyora-doc-api/app/` exige un **rebuild + recreate**. Un simple restart ne recharge rien.
|
||||
|
||||
## Procédure validée (depuis mcp-nas ou Mac)
|
||||
|
||||
1. Docker indisponible dans mcp-nas (socket retiré, audit 08/07) → passer par le Mac : `ssh -p 22222 Best0f@192.168.100.33`. Sur le host : docker = `/usr/local/bin/docker`, **sans sudo** (Best0f dans le groupe docker ; sudo échoue, pas de TTY).
|
||||
2. Build : `cd /volume1/docker/nyora-doc-api && /usr/local/bin/docker compose build` (~2 min, pip timeout 180 déjà dans le Dockerfile).
|
||||
3. Déploiement : `docker stop nyora-doc-api && docker rm nyora-doc-api && docker compose up -d` — **jamais un simple restart**, l'image ne serait pas rechargée. Alternative sans SSH : `nohup` côté host (cf. session 08/07).
|
||||
4. Health : `curl http://172.17.0.1:3050/health` → 200.
|
||||
5. Vérifier code container == disque == Gitea : `docker exec nyora-doc-api cat /app/services.py | diff - /volume1/docker/nyora-doc-api/app/services.py`.
|
||||
|
||||
## Test /v1/generate — pièges
|
||||
|
||||
- Endpoint **multipart/form-data**, pas JSON : `curl -F "format=docx" -F "data=</tmp/data.json"` + header `X-API-Key` (clé dans hermes-platform/.env). Un POST -d JSON → 422 "body.format missing".
|
||||
- Payload avec apostrophes françaises : toujours dans un fichier, jamais inline.
|
||||
|
||||
## Historique (clos)
|
||||
|
||||
- 03-04/07 : footer Nyora ajouté aux DOCX (n'avait jamais existé) puis déployé.
|
||||
- **08/07 (commits a7380268 + f7353419) : footers retirés de TOUS les formats** (DOCX/PPTX/PDF/XLSX) sur décision Nabil, et option `restyle=full` implémentée pour le mode template (voir common/nyora-doc-api-template-restyle-cause-racine.md). Les vérifications XML de footer des anciennes versions ne s'appliquent plus.
|
||||
|
||||
Guide d'usage API : common/nyora-doc-api-hermes-guide.md.
|
||||
@@ -0,0 +1,65 @@
|
||||
# Migration provider → OpenCode Go (consolidé)
|
||||
|
||||
**Date** : 2026-07-09 (fusionne migration-provider-opencode-go.md 14/06 + OPENCODE-GO-MIGRATION.md 16/06 ; état mis à jour)
|
||||
**Tags** : infra, opencode, hermes, family-help, bifrost, fallback
|
||||
|
||||
## État actuel des modèles (09/07/2026)
|
||||
|
||||
| Instance | model.default | Fallback |
|
||||
|---|---|---|
|
||||
| hermes-tt | deepseek-v4-flash (opencode-go) | opencode-go/deepseek-v4-flash |
|
||||
| hermes-nyora | **mimo-v2.5** (verdict pilote 08/07 : GARDER — voir hermes-nyora/verdict-pilote-mimo-v25.md) | opencode-go/deepseek-v4-flash |
|
||||
| hermes-perso | deepseek-v4-flash (opencode-go) | opencode-go/deepseek-v4-flash |
|
||||
|
||||
⚠️ Depuis le 06/07, les fallbacks pointent vers **opencode-go** (forfait) et non plus openrouter (payant) — voir common/bifrost-vk-incidents.md.
|
||||
|
||||
**Source de vérité compose** : `/volume1/docker/hermes-platform/docker-compose.yml` (top-level), confirmé par le fix DNS du 09/07 (recreate des 3 agents). Les compose per-instance sont en `.disabled` ; les mentions historiques « stacks Portainer 291/292/294, top-level obsolète » ne sont plus valables.
|
||||
|
||||
## Endpoint & modèles OpenCode Go
|
||||
|
||||
Provider built-in `opencode-go` : base_url `https://opencode.ai/zen/go/v1`, auth `Authorization: Bearer <clé>` uniquement (pas de x-bf-vk, pas de HTTP-Referer/X-Title). IDs **sans préfixe** (`deepseek-v4-flash`, `glm-5.1`, `mimo-v2.5`…), jamais `openrouter/` ni `deepseek/`.
|
||||
Utilisables en OpenAI-compat : deepseek-v4-flash/pro, glm-5/5.1, qwen3.6/3.7-plus, kimi-k2.6, kimi-k2.7-code, minimax-m3, mimo-v2.5(-pro).
|
||||
|
||||
**NE FONCTIONNE PAS** :
|
||||
- `qwen3.7-max` → `Model not supported for format oa-compat` (dispo seulement format Anthropic). Consommateurs OpenAI-compat → `glm-5.1`.
|
||||
- MiniMax M2.7 vision = `prompt_tokens:0` (vision cassée). Vision/OCR → Gemini via Bifrost.
|
||||
|
||||
## config.yaml Hermes
|
||||
|
||||
`/volume1/docker/hermes-platform/hermes-{tt,nyora,perso}/data/config.yaml` :
|
||||
```yaml
|
||||
model:
|
||||
default: <modele>
|
||||
provider: opencode-go
|
||||
fallback_providers:
|
||||
- provider: opencode-go
|
||||
model: deepseek-v4-flash
|
||||
key_env: OPENCODE_GO_API_KEY # nom réel injecté par le compose top-level (PAS OPENCODE_API_KEY)
|
||||
```
|
||||
Hermes **réécrit config.yaml au shutdown** → toujours `docker stop` → éditer → `docker start` (jamais éditer à chaud). Healthcheck port 8642, API `/v1/chat/completions`, modèle proxy `hermes-agent`.
|
||||
⚠️ data/ en 0700 uid 1026 : illisible depuis mcp-nas post-hardening — édition via Mac ou canal autorisé.
|
||||
|
||||
## family-help
|
||||
|
||||
`medical_service.py` → helper `_llm_complete()` (OpenCode primaire, OpenRouter fallback web/erreur), routing par skill : medical/nutrition glm-5.1, homework qwen3.7-plus, legal_admin deepseek-v4-pro, tech/general deepseek-v4-flash. **`max_tokens ≥ 2048`** obligatoire (modèles de raisonnement : le reasoning mange le budget avant le content → `finish_reason=length`, content vide). Même piège vu sur mimo-v2.5 (fix : max_tokens 8192 sur hermes-nyora).
|
||||
|
||||
## Bifrost (rappel)
|
||||
|
||||
Bifrost ne recharge PAS les clés provider au restart → `config.json` chargé au boot + `allow_all_keys=1` en DB. Providers dans config.json ≠ store (`DELETE FROM config_providers/config_keys`, bifrost stoppé). Éditer via l'UI peut re-casser. Détails : common/bifrost-access.md et common/bifrost-vk-incidents.md.
|
||||
|
||||
## n8n
|
||||
|
||||
- Code nodes LLM → Bifrost obligatoire (common/runbook-n8n-llm-bifrost.md). `require('https')` + parsing manuel, jamais fetch/new URL/$helpers.
|
||||
- 15 workflows AO/veille archivés contiennent encore OpenRouter : l'API n8n refuse d'éditer un archivé. Pour réutiliser : désarchiver (UI) → migrer le Code node (endpoint zen/go/v1, clé OpenCode, retirer HTTP-Referer/X-Title) → re-tester → ré-archiver.
|
||||
- Restent volontairement sur OpenRouter direct : `Mode-nedya`, `FAMILLE | Recettes Weekend`, `Labellisation Auto`.
|
||||
|
||||
## Pièges DSM/NAS
|
||||
|
||||
- `docker` hors PATH SSH → `/usr/local/bin/docker` (Best0f groupe docker, sans sudo).
|
||||
- `hermes model` / `hermes fallback add` en SSH → « requires an interactive terminal » → éditer le YAML.
|
||||
- Vérif : `docker exec hermes-agent-<i> hermes fallback ls` + test curl `localhost:8642/v1/chat/completions`.
|
||||
|
||||
## Références
|
||||
|
||||
- https://opencode.ai/docs/zen · hermes-agent.nousresearch.com/docs (fallback-providers)
|
||||
- hermes-nyora/verdict-pilote-mimo-v25.md · common/bifrost-vk-incidents.md
|
||||
@@ -1,34 +0,0 @@
|
||||
# Cron Hermes ne se relit pas au vol + publish n8n casse : veille Dr Nexum non recue
|
||||
|
||||
**Date** : 2026-07-07
|
||||
**Tags** : infra, cron, hermes, n8n, publish, timezone, veille, nyora
|
||||
|
||||
## Symptome
|
||||
Veille Dr Nexum (Hermes 3x/jour + workflow n8n) non recue le 06/07 20h ni le 07/07 7h, sur les deux pipelines simultanement.
|
||||
|
||||
## Cause 1 -- Cron Hermes fige en memoire (jobs.json)
|
||||
Le service cron de chaque agent Hermes charge `jobs.json` uniquement au demarrage du container. Une edition directe du fichier (passage de 1x a 3x/jour le 06/07) s'est bien ecrite sur disque, mais le processus tournait toujours avec l'ancien horaire quotidien (11h30 UTC) en memoire -- `next_run_at` restait fige sur l'ancienne valeur meme un jour apres l'edit.
|
||||
|
||||
**Fix** :
|
||||
1. Editer `jobs.json` (cron expr).
|
||||
2. Recalculer et ecrire manuellement `next_run_at` a la bonne valeur (le daemon ne le fait pas toujours correctement au premier boot suivant).
|
||||
3. Redemarrer le container **agent** correspondant -- nom reel `hermes-agent-tt` / `hermes-agent-nyora` / `hermes-agent-perso` (piege : ne pas confondre avec `hermes-workspace-*`, qui sont d'autres containers).
|
||||
4. Restart via API Portainer : `POST /api/endpoints/2/docker/containers/{id}/restart` timeout systematiquement sur cette instance -- utiliser `stop` puis `start` explicites (2 appels separes).
|
||||
5. Verifier apres coup que `next_run_at` a la bonne valeur.
|
||||
|
||||
Portainer : endpoint local = id `2`. Auth via `172.17.0.1:9000/api/auth` depuis un container (jamais l'IP LAN). Credentials dans `hermes-platform/.env`.
|
||||
|
||||
## Cause 2 -- Fuseau horaire (NAS + n8n en UTC, pas Tunis)
|
||||
Le NAS et n8n tournent en UTC. Tunis = UTC+1 toute l'annee (pas de changement d'heure). Un cron `0 7,12,20 * * *` tire a 8h/13h/21h Tunis, pas 7h/12h/20h.
|
||||
|
||||
**Fix** : toujours retirer 1h a l'expression cron pour un horaire "heure de Tunis" -- `0 6,11,19 * * *` en UTC pour obtenir 7h/12h/20h Tunis. Applique sur jobs.json (Hermes) et sur le node Schedule Trigger n8n.
|
||||
|
||||
## Cause 3 -- publish_workflow n8n casse sur un workflow specifique (non systemique)
|
||||
Un workflow deja actif, modifie via l'outil `update_workflow`, ne bascule pas automatiquement en production -- `update_workflow` cree un nouveau `versionId` (brouillon) mais le workflow continue de servir son `activeVersionId` existant. Sur ce workflow precis, `publish_workflow` echouait de facon persistante avec `"Version not found"`, y compris en reessayant **depuis l'UI n8n elle-meme** (capture d'ecran a l'appui) et apres un `unpublish_workflow` prealable -- confirme comme un etat corrompu propre a ce workflow (chaine de versions cassee en base), pas un bug de l'outil MCP.
|
||||
|
||||
**Fix qui a marche** : dupliquer le workflow (bouton **Duplicate** dans l'UI n8n). La copie repart avec une chaine de versions saine et se publie normalement des le premier essai. Renommer la copie pour retirer le suffixe " copy", puis `unpublish_workflow` + `archive_workflow` sur l'original casse une fois la copie confirmee active.
|
||||
|
||||
**Verification obligatoire apres toute modif d'un workflow n8n deja actif** : `get_workflow_details` -> comparer `versionId` et `activeVersionId`. S'ils different, le changement n'est PAS en production, meme si l'outil de modification a repondu succes.
|
||||
|
||||
## Lecon generale
|
||||
Une reponse "success" d'un outil de modification (`update_workflow`, edition de `jobs.json`) ne garantit jamais que le changement est en production. Toujours verifier l'etat reel apres coup : `next_run_at` recalcule pour un cron Hermes redemarre, `activeVersion == draft` pour un workflow n8n.
|
||||
@@ -1,44 +0,0 @@
|
||||
# Runbook - Veille Hebdo Dr. Nexum (Kanban Fan-Out)
|
||||
|
||||
Date : 2026-06-18 | Instance : hermes-nyora | Statut : Deploye
|
||||
|
||||
## Architecture
|
||||
|
||||
Cron lundi 07h00 -> default (orchestrateur) -> 6 workers veilleur paralleles -> synthese brief Telegram
|
||||
|
||||
## Profile veilleur
|
||||
|
||||
Path : /opt/data/profiles/veilleur/
|
||||
SOUL.md : scoring /10, angle video, signal fort OUI/NON
|
||||
Config : deepseek-v4-flash/openrouter, max_turns 25
|
||||
|
||||
## Sources (6)
|
||||
|
||||
youtube-fr-ia : videos IA/automatisation FR semaine
|
||||
huggingface-blog : nouveaux modeles open-source
|
||||
reddit-ia : r/LocalLLaMA + r/MachineLearning
|
||||
producthunt-ia : nouveaux outils IA lances
|
||||
labs-ia : Anthropic + OpenAI + DeepMind annonces
|
||||
automatisation : n8n + Make + Zapier blog workflows
|
||||
|
||||
## Sortie
|
||||
|
||||
Fiches sources : /opt/data/workspace/veille/YYYY-WNN/<source>.md
|
||||
Brief final : /opt/data/workspace/veille/YYYY-WNN/brief-nexum.md
|
||||
Livraison : Telegram Nabil (2084513684)
|
||||
|
||||
## Cron jobs.json
|
||||
|
||||
Fichier : /volume1/docker/hermes-platform/hermes-nyora/data/cron/jobs.json
|
||||
Job ID : veille-hebdo-nexum-kanban-a1b2c3d4
|
||||
Expr : 0 7 * * 1 (lundi 07h00 UTC = 08h00 Tunisie ete)
|
||||
|
||||
## Declencher manuellement
|
||||
|
||||
Depuis Telegram hermes-nyora :
|
||||
Lance la veille hebdo Nyora maintenant
|
||||
|
||||
## Deux crons hermes-nyora actifs
|
||||
|
||||
1. Veille IA Matin (11h30 UTC quotidien) - opportunites Dr. Nexum via veille_ia_matin.py
|
||||
2. Veille Hebdo Fan-Out (07h00 lundi) - analyse profonde 6 sources, brief structure
|
||||
@@ -1,94 +0,0 @@
|
||||
# Workflow Claude-GLM -- Nyora API Builder
|
||||
|
||||
**Date** : 2026-06-17 (rev 2)
|
||||
**Scope** : hermes-nyora (GLM-5.1 via opencode-go) + Telegram bot
|
||||
**Objectif** : Economiser les tokens Claude -- garder Claude pour le strategique
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
PLANNER -> Claude (discussion + accord + generation plan)
|
||||
EXECUTOR -> hermes-nyora / GLM-5.1 via Telegram (implementation)
|
||||
REVIEWER -> Claude (validation finale, problemes critiques uniquement)
|
||||
|
||||
Ratio economies tokens : ~7x (Claude = 20-30k, GLM = 150k+)
|
||||
|
||||
---
|
||||
|
||||
## Workflow complet (4 etapes)
|
||||
|
||||
### Etape 1 -- Discussion et accord (session Claude)
|
||||
|
||||
Nabil decrit son projet librement a Claude.
|
||||
Claude pose les questions necessaires :
|
||||
- Stack existante et contraintes
|
||||
- Perimetre exact de l API / feature
|
||||
- Integrations (Bifrost, Baserow, n8n, etc.)
|
||||
- Chemin du repo Gitea
|
||||
|
||||
Pas de template a remplir avant -- la discussion IS le template.
|
||||
Accord explicite avant de passer a l etape 2.
|
||||
|
||||
### Etape 2 -- Generation du plan (Claude)
|
||||
|
||||
Claude genere un PLAN.md complet et structure.
|
||||
Format : PROJECT / CONTEXT / TECH STACK / PHASES / REVIEW CRITERIA
|
||||
Maximum 5 phases. Chaque phase : GOAL + TASKS + FILES + VALIDATION.
|
||||
Claude inclut les criteres de review qu il utilisera a l etape 4.
|
||||
|
||||
### Etape 3 -- Execution (Telegram -> hermes-nyora)
|
||||
|
||||
Nabil copie-colle le PLAN.md dans le Telegram bot hermes-nyora.
|
||||
hermes-nyora active automatiquement le skill claude-glm-workflow.
|
||||
Execution phase par phase avec rapport standardise.
|
||||
hermes-nyora genere HANDOFF.md a la fin.
|
||||
|
||||
Bot Telegram hermes-nyora : token 8914117708:AAHNysUFMy8eFRagLmcNMvCkiCuArf0aZMs
|
||||
allowed_chats : '' (tous les chats autorises par defaut)
|
||||
|
||||
### Etape 4 -- Review finale (session Claude)
|
||||
|
||||
Nabil copie HANDOFF.md + prompt review dans Claude.
|
||||
Prompt review disponible dans : workspace/templates/claude-plan-template.md
|
||||
Claude identifie uniquement les problemes critiques et majeurs.
|
||||
Verdict : Ready to deploy | Needs fixes.
|
||||
|
||||
---
|
||||
|
||||
## Fichiers deployes
|
||||
|
||||
hermes-nyora/data/skills/nyora/claude-glm-workflow/SKILL.md
|
||||
hermes-nyora/data/workspace/templates/claude-plan-template.md
|
||||
hermes-nyora/data/SOUL.md (section WORKFLOW CLAUDE-GLM)
|
||||
|
||||
---
|
||||
|
||||
## Modele et couts
|
||||
|
||||
Modele actuel : glm-5.1 (opencode-go, $10/mois = $60 valeur)
|
||||
Fallback : qwen3.7-plus (opencode-go)
|
||||
GLM-5.2 : disponible semaine 22 juin -- changer config.yaml model.default
|
||||
|
||||
---
|
||||
|
||||
## Pieges connus
|
||||
|
||||
- SOUL.md et config.yaml : 100% ASCII obligatoire
|
||||
- hermes-nyora voit /opt/data/workspace/ (pas /workspace/)
|
||||
- Bifrost requis pour tout LLM depuis n8n (opencode.ai inaccessible)
|
||||
---
|
||||
|
||||
## Bonne pratique -- Plans longs (> limite Telegram)
|
||||
|
||||
Pour tout plan depasse quelques centaines de caracteres :
|
||||
|
||||
1. Claude depose le PLAN.md dans /opt/data/workspace/plans/<projet>.md via MCP
|
||||
2. Nabil envoie UNE LIGNE dans Telegram :
|
||||
Execute le plan : /opt/data/workspace/plans/<projet>.md
|
||||
3. hermes-nyora lit le fichier et execute normalement
|
||||
|
||||
Avantages : plans illimites en taille, historique des plans conserve sur le NAS,
|
||||
relance facile sans re-coller le prompt, traçabilite Gitea possible.
|
||||
|
||||
Convention nommage : <slug-projet>-YYYYMMDD.md (ex: panda-theme-switcher.md)
|
||||
Reference in New Issue
Block a user