7.3 KiB
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.mdv1.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 exposesize,mtime(epoch) etmtime_iso(issus de l'index — source unique, même contrat quelist_all_files) ; les répertoires portentNone.GET /api/browse/{vault}:modified/modified_isosur les fichiers.- Outil MCP
list_directory:size/mtime/mtime_isosur les fichiers. - Cohérence read-after-write :
PUT /api/file/{vault}/saverafraî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_fileexigeaient 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) etpath_prefix(égalité ou sous-arbre —MaisonmatcheMaison/a.md, pasMaison2/a.md; sur renommage/déplacement,old_pathounew_pathsuffit pour être prévenu). Sans filtre : reçoit tout, comme avant (compatibilité). POST /api/webhooksacceptevault/path_prefix;PATCHles modifie (""réinitialise) ;GETles expose (WebhookModel.vault/path_prefix).PUT /api/file/{vault}/saveémet désormaisfile_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, avecvault: "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/tokensacceptevaults(sous-ensemble des vaults du créateur, défaut = tous —400si inconnu,403si hors d'accès) etscope("full"défaut |"files"). - Restriction vault : le JWT embarque les vaults autorisés →
check_vault_access(REST) etrequire_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 viaTokenScopeMiddleware(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 horsFILES_SCOPE_TOOLS(token_scope_denied),list_toolsfiltré pour ces tokens, ressourcesvault://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) :
# 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 :
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) |