feat: intégration TikTak #197-200 + scoped tokens, webhooks filtrés, listes mtime/size ; fix e2e-server BUG-112
CI / lint (push) Successful in 3m17s
CI / security (push) Failing after 1m51s
CI / test (push) Successful in 5m14s
CI / build (push) Successful in 1m50s
CI / e2e (push) Successful in 18m18s

This commit is contained in:
2026-10-14 11:42:53 -04:00
parent 2be0b15bcf
commit f783394f09
59 changed files with 3233 additions and 77 deletions
+132
View File
@@ -0,0 +1,132 @@
# Intégration TikTak ↔ ObsiGate — socle côté ObsiGate (#197 → #200)
> **Statut :** ✅ livré — **IDs stables :** #197, #198, #199, #200
> **Issues Gitea :** Projets/ObsiGate #4, #3, #2, #1 (même ordre)
> **Référence d'analyse :** `Projets/ticktick-clone`, `docs/SYNCHRO-OBSIGATE.md` v1.1
> (décisions §8 : vault dédié « TikTak », transport API REST, TikTak maître en conflit)
## Contexte
TikTak projette ses tâches/notes en fichiers Markdown dans un vault ObsiGate dédié
via l'API REST fichiers. Côté ObsiGate, quatre manques bloquaient une synchronisation
propre : réconciliation sans relire chaque fichier (#197), webhooks filtrables (#198),
clé API à moindre privilège (#199), procédure de provisionnement (#200).
## #197 — Listes de fichiers : `mtime` + `size` (Gitea #4)
**Constat.** `GET /api/vault/{vault}/files` et l'outil MCP `list_all_files` exposaient
déjà `size`/`modified`, mais pas `GET /api/vault/{vault}/paths` (seulement
`vault/path/name/type`), ni `GET /api/browse/{vault}`, ni l'outil `list_directory`.
**Livré.**
- `GET /api/vault/{vault}/paths` : chaque entrée fichier expose `size`, `mtime`
(epoch) et `mtime_iso` (issus de l'index — source unique, même contrat que
`list_all_files`) ; les répertoires portent `None`.
- `GET /api/browse/{vault}` : `modified`/`modified_iso` sur les fichiers.
- Outil MCP `list_directory` : `size`/`mtime`/`mtime_iso` sur les fichiers.
- Cohérence read-after-write : `PUT /api/file/{vault}/save` rafraîchit l'index de
façon synchrone (`update_single_file`), au lieu de dépendre du watcher.
- **Bug latent corrigé au passage** : `update_single_file`/`remove_single_file`
exigeaient un chemin absolu alors que toutes les routes leur passent un chemin
relatif au vault — le rafraîchissement post-écriture était un no-op silencieux
(masqué en prod par le watcher). Les deux fonctions acceptent désormais les deux
formes.
**Réconciliation type (un seul appel) :** snapshot `{path: (mtime, size)}`, puis
re-liste et comparaison — démontré par
`tests/test_sync_lists.py::TestReconciliationScenario`.
## #198 — Webhooks : `vault` + filtre `vault` / `path_prefix` (Gitea #3)
**Constat.** Les charges utiles contenaient déjà `vault`, mais sans filtre un
consommateur recevait tous les vaults. De plus `file_modified` (événement souscrit
mais jamais émis) manquait pour tirer les retours de synchro.
**Livré.**
- Filtres optionnels par webhook : `vault` (égalité exacte) et `path_prefix`
(égalité ou sous-arbre — `Maison` matche `Maison/a.md`, pas `Maison2/a.md` ;
sur renommage/déplacement, `old_path` **ou** `new_path` suffit pour être prévenu).
Sans filtre : reçoit tout, comme avant (compatibilité).
- `POST /api/webhooks` accepte `vault`/`path_prefix` ; `PATCH` les modifie
(`""` réinitialise) ; `GET` les expose (`WebhookModel.vault/path_prefix`).
- `PUT /api/file/{vault}/save` émet désormais `file_modified`
(`{"vault", "path"}`) — la sauvegarde tableur/IA reste hors périmètre (documenté).
- UI : champs filtre à la création + badge dans la liste ; aide intégrée FR/EN
mise à jour (`file_modified` émis, filtrage documenté).
- Filtrage pur et testable : `backend/webhooks.py::webhook_matches` ;
`tests/test_webhook_filters.py` (matching, persistance, dispatch simulé :
le webhook filtré sur « TikTak » ne reçoit que ses événements, avec
`vault: "TikTak"` dans la charge).
## #199 — Clés API à portée limitée (Gitea #2)
**Constat.** Les clés longue durée portaient toutes les permissions du compte.
**Livré.**
- Création : `POST /api/auth/tokens` accepte `vaults` (sous-ensemble des vaults
du créateur, défaut = tous — `400` si inconnu, `403` si hors d'accès) et
`scope` (`"full"` défaut | `"files"`).
- Restriction vault : le JWT embarque les vaults autorisés → `check_vault_access`
(REST) et `require_vault_access` (MCP/outils) refusent le reste en 403.
- Périmètre `files` : fichiers (CRUD, listes, recherche, backups lecture/restauration),
`GET /api/vaults`, `/api/search*`, `/mcp`. Tout le reste (admin, config, IA,
webhooks, partages, **gestion des clés elle-même** — anti-escalade) → **403**
via `TokenScopeMiddleware` (`backend/auth/scope.py`, stateless : claim lu sans
vérification pour le refus, signature toujours vérifiée en aval).
- MCP : `call_tool` + `propose_*` refusent les outils hors `FILES_SCOPE_TOOLS`
(`token_scope_denied`), `list_tools` filtré pour ces tokens, ressources
`vault://` couvertes.
- UI : sélecteur de portée + champ vaults à la création, badge de portée et
vaults dans la liste (i18n FR/EN).
- Compatibilité : clés existantes (sans `scope`) = `full`, comportement inchangé.
- Tests : `tests/test_scoped_tokens.py` (13 cas : création/validation, lecture/
écriture OK, 403 autre vault, 403 hors périmètre, non-régression full, outils).
## #200 — Provisionnement du vault « TikTak » (Gitea #1, exploitation)
Opération unique côté hébergement (aucun code requis — endpoints déjà testés) :
```bash
# 1. Dossier sur l'hôte
mkdir -p /NFS/OBSIDIAN_DOC/TikTak
# 2. Montage conteneur (docker-compose.yml, puis recreate)
# volumes:
# - /NFS/OBSIDIAN_DOC/TikTak:/vaults/TikTak
# 3. Déclarer le vault SANS redémarrage (admin, persisté via data/vaults.json)
curl -X POST $BASE/api/vaults/add -H "Authorization: Bearer $ADMIN" \
-H 'Content-Type: application/json' -d '{"name":"TikTak","path":"/vaults/TikTak"}'
# 4. Octroyer le vault au compte de synchro (ACL par vault, UI admin ou API)
# 5. Clé à portée limitée pour TikTak (cf. #199)
curl -X POST $BASE/api/auth/tokens -H "Authorization: Bearer $ADMIN" \
-H 'Content-Type: application/json' \
-d '{"name":"TikTak sync","expiry":"never","vaults":["TikTak"],"scope":"files"}'
# 6. Webhook retour filtré (optionnel, phase 3 TikTak)
curl -X POST $BASE/api/webhooks -H "Authorization: Bearer $ADMIN" \
-H 'Content-Type: application/json' \
-d '{"name":"TikTak retour","url":"https://tiktak…/hooks/obsigate","events":["file_created","file_modified","file_deleted","file_renamed"],"vault":"TikTak"}'
```
**Acceptation :** le vault « TikTak » est visible ; un `.md` créé via
`POST /api/file/TikTak` (clé limitée, pas admin) est lu, cherché et édité sans erreur :
```bash
KEY=<clé de l'étape 5>
curl -X POST $BASE/api/file/TikTak -H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' -d '{"path":"Inbox/sonde.md","content":"# sonde\n"}'
curl -s "$BASE/api/file/TikTak/raw?path=Inbox/sonde.md" -H "Authorization: Bearer $KEY"
curl -s "$BASE/api/search?q=sonde&vault=TikTak" -H "Authorization: Bearer $KEY"
```
## Fichiers touchés (rappel)
| # | Backend | Frontend | Tests |
|---|---|---|---|
| 197 | `services/search.py`, `services/vaults.py`, `schemas.py`, `indexer.py`, `routers/files_write.py`, `tools/service.py` | — | `tests/test_sync_lists.py` |
| 198 | `webhooks.py`, `schemas.py`, `routers/webhooks.py`, `routers/files_write.py` | `js/config.js`, `index.html`, `locales/{fr,en}.json` | `tests/test_webhook_filters.py` |
| 199 | `auth/jwt_handler.py`, `auth/middleware.py`, `auth/scope.py` (nouveau), `auth/router.py`, `main.py`, `tools/context.py`, `tools/registry.py`, `tools/api.py`, `mcp/server.py`, `openapi_docs.py` | `js/config.js`, `index.html`, `locales/{fr,en}.json` | `tests/test_scoped_tokens.py` |
| 200 | — (runbook) | — | — (recette manuelle ci-dessus) |