feat: émission d'événements vers le journal du service d'agents #220
CI / lint (push) Successful in 2m53s
CI / security (push) Failing after 1m46s
CI / test (push) Successful in 4m57s
CI / build (push) Successful in 1m36s
CI / e2e (push) Successful in 17m40s

This commit is contained in:
Spark
2026-10-22 19:58:25 +00:00
parent f4b8935189
commit f434f30286
14 changed files with 697 additions and 20 deletions
@@ -0,0 +1,98 @@
# #220 — Fusion : émission d'événements vers le journal du service d'agents
**Contexte.** Le Lot 2 du programme Fusion (carte fusion-trois-apps
#18) dote le service d'agents d'un **journal d'activité** cross-apps :
chaque application y émet ses mutations significatives au contrat
**ACC-0.2** — enveloppe `{id, type, occurred_at, actor, domain,
source_app, object{ref, label, url}}` signée **HMAC-SHA256** du corps
brut (en-tête `X-Draco-Signature`, clé propre à chaque application),
que le service expose dans la vue Journal de sa console (agent-commun
0.11.0). TikTak (0.59.0) et Kanbante (0.28.0) émettent déjà ; ObsiGate
est la troisième source, après Flowdeck.
**Principe directeur : référence, jamais contenu.** L'enveloppe ne
contient que la référence de la note (`fichier:<vault>/<chemin>`),
son nom (libellé) et un lien profond (`/#file=<vault>:<chemin>`) —
jamais son contenu.
## Garde-fou central : jamais le watcher
ObsiGate surveille les vaults par un **watcher** de système de
fichiers (`backend/watcher.py`) qui réindexe chaque changement
disque — le vault principal compte **222 393 fichiers**. Brancher
l'émission sur le watcher inonderait le journal (et confondrait les
gestes d'Obsidian, de la synchro de fichiers ou d'un éditeur externe
avec des opérations faites *dans* ObsiGate).
L'émission n'a donc lieu que pour les **opérations explicites via
l'API web**, à deux points d'accroche, et nulle part ailleurs :
| Source | Point d'accroche | Acteur |
|---|---|---|
| Interface web / clients REST | `backend/routers/files_write.py` | `user` |
| Serveur MCP d'ObsiGate (outils) | `backend/tools/service.py`, mode `mcp` | `agent:mcp` |
| Assistant intégré (outils, mode `in_app`) | `backend/tools/service.py` | `user` |
Conséquences assumées :
- une note créée/modifiée **hors API** (Obsidian, éditeur externe,
copie de fichiers) n'apparaît **pas** au journal — c'est voulu ;
- les écritures internes qui passent par la couche mutations sans
passer par ces points (planificateur, crawler, fusion de doublons,
restauration de sauvegarde) n'émettent pas non plus ;
- le test `test_watcher_et_indexation_sans_evenement` verrouille le
garde-fou : écriture disque directe + gestionnaires du watcher +
`update_single_file` de l'indexeur → **zéro émission**.
## Catalogue d'événements émis
| Type | Déclencheur |
|---|---|
| `obsigate.note.created` | `POST /api/file/{vault}` ; outil `create_file` |
| `obsigate.note.edited` | `PUT /api/file/{vault}/save` (+ variantes xlsx/csv : cellules, structure, style) ; outils `edit_file`, `append_to_file` |
| `obsigate.note.moved` | `PATCH /api/file/{vault}` (renommage), `POST /api/move/{vault}` (**fichiers seulement**) ; outils `rename_file`, `move_path` (fichiers seulement) — la référence pointe sur le **nouveau** chemin |
| `obsigate.note.deleted` | `DELETE /api/file/{vault}` ; outil `delete_file` |
Hors catalogue (non émis, volontairement) : dossiers (création,
renommage, déplacement, suppression — le catalogue est `note.*`),
téléversement par lots (`batch-upload`), duplication, restauration
de sauvegarde, `replace_in_files` (multi-fichiers).
## Module `backend/agent_journal.py`
- **Activation** : `AGENT_EVENEMENTS_CLE` (clé propre à ObsiGate,
jamais en dur). Sans clé, le module est **inactif** : aucun appel,
aucun bruit. URL surchargée par `AGENT_EVENEMENTS_URL` (défaut
`https://agent.dracodev.net/api/v1/evenements`).
- **Discipline « au mieux »** (identique à TikTak/Kanbante) : envoi
dans un fil d'arrière-plan démon via httpx, délai maximal 2 s,
exceptions avalées avec une ligne de journal local — une panne du
service d'agents ne casse ni ne ralentit jamais le métier (prouvé
par `test_recepteur_en_panne_ne_casse_rien`).
- **Domaine** : ObsiGate ne modélise pas encore le domaine d'un
vault (la question Q-A du programme — les domaines sont-ils des
vaults distincts ? — est toujours ouverte). La correspondance peut
être déclarée par `AGENT_EVENEMENTS_DOMAINES` (JSON
`{"NomDuVault": "famille"}`) ; repli `pro`, qui est aussi le
défaut du journal du service.
## Activation en production (gestes de Bruno)
1. Générer une clé propre à ObsiGate (`openssl rand -hex 32`).
2. Service d'agents : ajouter l'entrée `obsigate` à
`AGENT_EVENEMENTS_CLES` (JSON des clés par application), recréer
le conteneur.
3. ObsiGate : poser `AGENT_EVENEMENTS_CLE=<même valeur>` dans son
environnement et **redéployer** (Docker).
Tant que la clé n'est pas posée des deux côtés, ObsiGate n'émet
rien — c'est le comportement voulu.
## Tests
`tests/test_agent_journal.py` (7 tests) : enveloppe et signature
revérifiées comme le fait le service ; cycle de vie complet en REST
(créer → éditer → renommer → déplacer → supprimer, acteur `user`) ;
chemin outils/MCP (`create/edit/rename/delete`, acteur
`agent:mcp`) ; inactivité sans clé ; garde-fou watcher/indexation ;
panne du récepteur sans effet métier ; correspondance de domaines.