# 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_rules` et `search_rules` : **aucun** controle `has_scope_access`, quel que soit l'appelant. - `get_agent_config` / `get_my_context` / `update_rule` : controlaient l'acces sur un champ `agent`/`name` **declare par le client dans les arguments**, jamais verifie contre la cle API reelle. Un agent authentifie GEMINI pouvait se declarer `CLAUDE` en 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) : ```sql 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 : 1. **Transport casse** : reecrit selon le pattern officiel du SDK (`connect_sse()` comme context manager async, un seul `SseServerTransport` partage au lieu d'un par connexion). 2. **Double envoi ASGI** : `connect_sse()` et `handle_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 de `Depends(get_current_agent)` sur ce endpoint precis). 3. **Chemin double** : `SseServerTransport("/mcp/messages")` + montage a `/mcp` -> le SDK calcule `root_path + endpoint` = `/mcp` + `/mcp/messages` = `/mcp/mcp/messages` (invalide). Fix : configurer le transport avec `/messages` seul. **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.