12 KiB
context-hub — correctif securite MCP + table memory_entries typee
Date : 30/07/2026 Contexte : Nabil a juge context-hub sous-exploite et decide de le faire evoluer en memoire IA dediee coding (best practices, erreurs communes), avec MD comme representation durable et l'API/MCP comme jonction read/write instantanee. Avant d'ouvrir a plus d'agents (Antigravity, futurs repos/IA), inspection du code reel a revele une faille et un modele de donnees inadapte.
Faille identifiee et corrigee
app/routes/mcp.py, handle_call_tool : la couche REST (api.py, X-API-Key -> get_current_agent) authentifie correctement, y compris pour ouvrir la connexion SSE /mcp. Mais le handler MCP global ne recevait aucun contexte d'identite lie a cette connexion :
get_rulesetsearch_rules: aucun controlehas_scope_access, quel que soit l'appelant.get_agent_config/get_my_context/update_rule: controlaient l'acces sur un champagent/namedeclare par le client dans les arguments, jamais verifie contre la cle API reelle. Un agent authentifie GEMINI pouvait se declarerCLAUDEen argument et lire/ecrire hors de son perimetre.
Fix : contextvars.ContextVar pose dans run_server() au moment de l'etablissement SSE (current_agent_var.set(agent), agent = identite reelle resolue par get_current_agent), lu par handle_call_tool a la place de arguments.get("agent"). Applique uniformement aux 7 tools, y compris get_ports/check_port qui n'etaient pas gates du tout cote MCP (alors que l'equivalent REST /api/ports exige le scope infra).
Nouveau modele : memory_entries
Le PATCH existant (app/models.py, set_rule) fait un dict.update() plat par scope — une cle reecrite ecrase silencieusement la precedente. Inadapte a une liste croissante de decisions/erreurs/best practices. Ajout d'une table dediee, sans toucher rules (aucune rupture pour infra/llm/nyora/perso/tt existants) :
memory_entries(id, scope, project, type, title, body, tags, status, superseded_by, created_by, created_at, updated_at)
memory_entries_fts(FTS5) + triggers de sync insert/update/delete.
type in {decision, constraint, best-practice, common-error, do-not-use} — id sequentiel genere par scope-abbr-NNNN (ex: coding-err-0007). status in {active, archived, superseded} — pas de suppression dure, tombstone via supersede_memory_entry/archive_memory_entry (meme principe que do-not-use.md dans MemoryCustodian, evalue puis ecarte au profit de cette integration native context-hub).
Fonctions ajoutees dans models.py : add_memory_entry, list_memory_entries, search_memory_entries (FTS5), supersede_memory_entry, archive_memory_entry. Testees en conditions reelles dans le container (insert/list/search/supersede) — toutes OK. Pas encore d'endpoint HTTP/MCP expose (prochaine phase : scope coding + tools record_lesson/get_context_pack).
Piege deploiement
docker compose up -d --build echoue depuis ssh nas-host (Best0f) : .env est en 600 root:root, Best0f (uid 1026) n'a pas les droits de lecture, et sudo necessite un mot de passe interactif (regle deja actee : pas de sudo agent). Pour un changement Python pur (pas de Dockerfile touche), pas besoin de --build : le bind mount ./app:/app/app est live, un simple docker restart context-hub suffit et evite le probleme .env. docker est joignable en direct sans sudo (/usr/local/bin/docker, Best0f est dans le groupe docker gid 65538) mais absent du PATH non-interactif SSH — utiliser le chemin complet.
Push Gitea sans git
git absent de mcp-nas et de la session SSH nas-host. Push via API Contents Gitea (PUT .../contents/{path} avec sha courant recupere par un GET prealable, POST si creation) plutot que git push — pattern deja documente dans common/git-push-depuis-mcp-nas.md, confirme fonctionnel ici aussi pour des fichiers Python (pas seulement Markdown).
Addendum (30/07/2026, meme session) : endpoint /mcp casse depuis toujours
Test end-to-end du correctif (demande explicite de Nabil, "teste-le toi-meme") a revele que /mcp n'avait
jamais fonctionne avec le SDK mcp 1.28.0 installe : SseServerTransport n'expose plus
handle_sse()/read_stream()/write_stream() (API d'une version anterieure). Trois bugs caches
par cette rupture d'API, corriges dans le meme passage :
- Transport casse : reecrit selon le pattern officiel du SDK (
connect_sse()comme context manager async, un seulSseServerTransportpartage au lieu d'un par connexion). - Double envoi ASGI :
connect_sse()ethandle_post_message()envoient chacun leur reponse HTTP completement par eux-memes. Les faire passer par des routes FastAPI classiques (Depends+return) fait que FastAPI tente un second envoi ->RuntimeError: Unexpected ASGI message 'http.response.start' sent, after response already completed. Fix : montage ASGI brut (router.mount), auth X-API-Key geree a la main (plus deDepends(get_current_agent)sur ce endpoint precis). - Chemin double :
SseServerTransport("/mcp/messages")+ montage a/mcp-> le SDK calculeroot_path + endpoint=/mcp+/mcp/messages=/mcp/mcp/messages(invalide). Fix : configurer le transport avec/messagesseul.
Verification finale : test end-to-end via vrai client MCP SDK (pas curl -- protocole SSE bidirectionnel
non testable simplement), commite dans app/test_security_mcp_context.py. 5 scenarios : GEMINI refuse sur
scope tt (lecture), GEMINI refuse meme en usurpant agent=CLAUDE dans les arguments (ancien vecteur),
GEMINI autorise sur son propre scope llm (non-regression), CLAUDE autorise sur tt (acces legitime
toujours fonctionnel), verification directe DB qu'aucune ecriture usurpee n'a atterri dans tt. Les 5
passent. Le endpoint MCP de context-hub est maintenant reellement fonctionnel et securise -- pas juste
"corrige sur le papier".
Phases 3-4-5-6 (30/07/2026, meme session) : scope coding, Markdown+Git, agents dynamiques, bootstraps
Phase 3 : scope coding ajoute (CLAUDE, GEMINI, NABIL -- pas les Hermes, agents
d'exploitation pas de dev). Tools MCP record_lesson (ecrit une entree typee) et
get_context_pack (charge selectif, filtrable par project). Teste : ecriture
CLAUDE et GEMINI, refus HERMES_TT (pas le scope), filtrage par projet.
Phase 4 : app/git_sync.py -- projection Markdown de memory_entries vers
docs/context-memory/{scope}.md, commit Gitea automatique (httpx, deja
dependance transitive de mcp) a chaque record_lesson. SQLite reste seul
lu par l'API (jamais le .md) ; le fichier n'est qu'une projection versionnee,
lisible et diffable. Teste : contenu genere verifie directement sur Gitea.
Piege trouve en cours de route : .env sur disque avait un GITEA_TOKEN
different de celui charge dans l'environnement du container (docker restart
ne relit pas env_file, seul un recreate le ferait -- bloque par les droits
root:600 sur .env pour Best0f). Les deux tokens restent valides actuellement
(verifie via /api/v1/user), donc pas bloquant aujourd'hui, mais si l'un est
revoque un jour sans recreate du container, le token vivant devient invalide
silencieusement. A surveiller lors d'une prochaine rotation.
Phase 5 : table agents (agent_name, agent_type, scopes JSON, repo_url),
remplace AGENT_SCOPES code en dur. has_scope_access/get_allowed_scopes
(app/scopes.py) restent volontairement synchrones -- lecture sqlite3 directe
(pas aiosqlite) pour ne toucher aucun des 13 points d'appel existants dans
api.py/mcp.py. Seed initial en upsert (migration depuis l'ancien dict).
Teste : non-regression CLAUDE/GEMINI sur tt, nouvel agent ajoute par simple
insertion SQL, sans redemarrage ni redeploiement, operationnel immediatement
(objectif central de la migration : brancher un futur repo/IA = une ligne DB +
une cle API + un bootstrap, zero modification de code).
Piege trouve : ecriture directe (cat >, open().write()) refusee sur
app/seed.py en particulier (Permission denied) alors que les autres fichiers
du meme dossier s'ecrivent normalement en root -- delete+recreate fonctionne
(rm puis nouveau fichier), pas de cause identifiee avec certitude (ACL
Synology au-dela des permissions POSIX affichees par ls -la suspectee).
Pattern a retenir : si str_replace/cat > echoue en Permission denied sur un
fichier NAS specifique alors que le dossier est normalement inscriptible,
essayer rm + recreation plutot que de s'acharner sur la modification en place.
Phase 6 : AGENTS.md (Gemini/Antigravity) mis a jour -- scope coding
ajoute au perimetre autorise, section 10 ajoutee expliquant la connexion MCP
(https://context.bolbol.tn/mcp) et les deux tools record_lesson/
get_context_pack (le scope coding n'a pas d'equivalent REST /api/rules/,
MCP uniquement). CLAUDE.md cree en miroir pour Claude Code, avec en plus
le protocole Git complet, la regle docker restart-vs-rebuild, et le principe
de fonctionnement acte avec Nabil (agir sans redemander a chaque action
courante). Cle API verifiee coherente avec la base avant modification.
Etat final : toute la batterie de tests (securite, phase3, phase4, phase5)
rejouee en sequence apres le dernier restart -- tout passe. Donnees de test
nettoyees, docs/context-memory/coding.md regenere vide. Migration context-hub
consideree complete pour cette iteration ; reste ouvert : endpoints REST pour
memory_entries (non fait, MCP suffit pour l'instant), connexion MCP reelle
d'Antigravity a valider cote client (le serveur est pret et teste, jamais
teste depuis Antigravity lui-meme plutot que le SDK Python).
Cloture des 2 pieges (30/07/2026, meme jour)
seed.py (Permission denied) : investigue plus a fond -- synoacltool -get confirme
le volume en mode Linux pur (aucune ACL Synology au-dela des permissions POSIX
standard). Reecriture directe retestee : fonctionne normalement. C'etait
transitoire (cause non identifiee avec certitude -- hypothese la plus probable :
verrou bref d'un service Synology comme l'indexation, jamais confirme), pas un
probleme structurel. Rien a corriger ; le contournement delete+recreate documente
plus haut reste valable si ca se reproduit.
Derive GITEA_TOKEN (.env) : root cause confirmee -- chgrp/chown refuses
meme en root depuis mcp-nas (capacite retiree, coherent avec le durcissement
post-audit RCE), et setfacl/getfacl absents (container et hote). Avant de
choisir un chmod 644 (seule option accessible sans intervention de Nabil), le
contenu complet de .env a ete verifie : il contient NAS_SSH_PASSWORD, le token
Baserow Zone Sud, ADMIN_PASSWORD/SECRET, JWT_SECRET -- bien trop sensible pour un
elargissement unilateral a "tout compte du NAS". Nabil a tranche : fix permanent
via sudo chgrp users .env && sudo chmod 640 .env (execute par lui en session
interactive root). Verifie : Best0f peut desormais lire .env. Container
recree (docker stop && docker rm && docker compose up -d -- un conflit de nom
a du etre resolu au passage, le compose ne remplace pas automatiquement un
container existant). GITEA_TOKEN vivant dans le container == GITEA_TOKEN dans
.env, derive resorbee. Batterie complete de tests rejouee post-recreate --
tout passe, aucune regression.
Lecon generalisable : tout .env root:600 sur ce NAS bloque de la meme
facon un recreate par Best0f. Si un autre container presente un jour le meme
symptome (docker compose up --build/up echoue avec "permission denied" sur
.env), le meme fix (chgrp users + chmod 640) s'applique, sous reserve de
verifier au prealable le contenu du fichier concerne (meme reflexe : ne jamais
elargir un acces sur un fichier de secrets sans en avoir lu le contenu reel).
Onboarding HERMES_NABIL (30/07/2026)
Agent ajoute avec acces complet (infra, llm, nyora, perso, tt, coding -- comme CLAUDE),
decision Nabil. Persiste dans agents table + .env (API_KEY_HERMES_NABIL) +
seed.py (survit a un restart/reseed, teste et confirme). Cle : voir .env,
non reproduite ici.
Reste bloque, cote VPS uniquement : hermes-nabil tourne sur le VPS Contabo, pas le NAS. mcp-vps ne peut pas atteindre l'hote reel (100.94.90.119) -- meme trou documente le 12/07 (aucune cle SSH deposee depuis mcp-vps vers authorized_keys de l'hote, container mcp-vps capacite-restreint sans acces Docker). Cote context-hub, tout est pret (agent + cle + acces complet verifie via MCP reel). Cote hermes-nabil (le service lui-meme sur le VPS), la configuration (pointer vers https://context.bolbol.tn/mcp avec cette cle) doit etre faite soit par Nabil directement, soit par Claude une fois la cle SSH mcp-vps -> hote deposee.