From 705f755b6b687c870b272aa37b6f16ce7553f040 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Tue, 22 Sep 2026 22:40:49 -0400 Subject: [PATCH] docs: guides d'utilisation, capture reelle et README ameliores --- AGENTS.md | 1 + CHANGELOG.md | 23 +- README.fr.md | 102 ++++--- README.md | 100 ++++--- VERSION | 2 +- desktop/Cargo.lock | 2 +- desktop/Cargo.toml | 2 +- desktop/tauri.conf.json | 2 +- docs/AI_ARCHITECTURE_GUIDE.md | 4 +- docs/GUIDES/API_REST.md | 326 +++++++++++++++++++++++ docs/GUIDES/ASSISTANT_IA_FORGE.md | 217 +++++++++++++++ docs/GUIDES/AUTHENTIFICATION_SECURITE.md | 229 ++++++++++++++++ docs/GUIDES/COLLABORATION.md | 86 ++++++ docs/GUIDES/DEPLOIEMENT_DOCKER.md | 221 +++++++++++++++ docs/GUIDES/DESKTOP.md | 201 ++++++++++++++ docs/GUIDES/MCP.md | 191 +++++++++++++ docs/GUIDES/PRISE_EN_MAIN.md | 236 ++++++++++++++++ docs/GUIDES/PWA_HORS_LIGNE.md | 136 ++++++++++ docs/GUIDES/README.md | 54 ++++ docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md | 193 ++++++++++++++ docs/MCP_GUIDE.md | 186 +------------ docs/ROADMAP.md | 2 +- docs/features/ai-tools-mcp.md | 2 +- docs/images/obsigate-home.png | Bin 0 -> 86067 bytes package.json | 2 +- 25 files changed, 2258 insertions(+), 262 deletions(-) create mode 100644 docs/GUIDES/API_REST.md create mode 100644 docs/GUIDES/ASSISTANT_IA_FORGE.md create mode 100644 docs/GUIDES/AUTHENTIFICATION_SECURITE.md create mode 100644 docs/GUIDES/COLLABORATION.md create mode 100644 docs/GUIDES/DEPLOIEMENT_DOCKER.md create mode 100644 docs/GUIDES/DESKTOP.md create mode 100644 docs/GUIDES/MCP.md create mode 100644 docs/GUIDES/PRISE_EN_MAIN.md create mode 100644 docs/GUIDES/PWA_HORS_LIGNE.md create mode 100644 docs/GUIDES/README.md create mode 100644 docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md create mode 100644 docs/images/obsigate-home.png diff --git a/AGENTS.md b/AGENTS.md index 3cd6c00..559e206 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -91,6 +91,7 @@ bash scripts/run-e2e-local.sh -g "nom du test" # filtre / --headed | Travail à venir + index | `docs/ROADMAP.md` | | Historique des versions | `CHANGELOG.md` | | Conception par feature | `docs/features/.md` | +| Guides d'utilisation | `docs/GUIDES/` | | Archive du complété | `docs/archive/COMPLETED_v1-v2.md` | | Bugs / TODO | `docs/ISSUES_TODOLIST.md` | | Build & releases | `docs/DEVELOPMENT_AND_RELEASES.md` | diff --git a/CHANGELOG.md b/CHANGELOG.md index d549356..851b93d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/), et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). > **En cours de développement** : les changements à venir sont listés dans la section -> [Unreleased](#unreleased). La dernière version livrée est **2.16.5**. +> [Unreleased](#unreleased). La dernière version livrée est **2.16.6**. --- @@ -14,6 +14,27 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [2.16.6] — 2026-09-22 + +### Ajouté + +- **Guides d'utilisation `docs/GUIDES/`** : nouvel index + 10 guides FR + (prise en main, recherche/PDF/Excalidraw, assistant IA & Forge, + collaboration temps réel, PWA & hors-ligne, API REST, serveur MCP, + authentification & sécurité, déploiement Docker, desktop Tauri). Le guide MCP + est déplacé dans `docs/GUIDES/MCP.md` ; `docs/MCP_GUIDE.md` devient une page + de redirection. + +### Modifié + +- **README.md / README.fr.md** : capture d'écran réelle de l'application en tête + (remplace l'illustration ASCII) ; un emoji sur chaque entrée de la table des + matières ; nouvelle section « Guides » avec liens vers `docs/GUIDES/` ; + renvois vers les guides depuis les sections API, Recherche, Sécurité, Desktop + et Collaboration. + +--- + ## [2.16.5] — 2026-09-22 ### Corrigé diff --git a/README.fr.md b/README.fr.md index 32034a9..577bf5d 100644 --- a/README.fr.md +++ b/README.fr.md @@ -1,61 +1,75 @@ # ObsiGate -> **Version française** — ce document est le miroir synchronisé de [README.md](README.md) (référence complète). Dernière synchronisation : juin 2026. +> **Version française** — ce document est le miroir synchronisé de [README.md](README.md) (référence complète). Dernière synchronisation : septembre 2026. **Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive. -[![Version](https://img.shields.io/badge/Version-2.16.5-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.16.6-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) [![CI/CD](https://img.shields.io/badge/CI%2FCD-Gitea_Actions-green.svg)](https://git.dracodev.net/Projets/ObsiGate/actions) -``` -┌─────────────────────────────────────────────────────────┐ -│ [🔍 Recherche...] [☀/🌙 Thème] ObsiGate │ -├──────────────┬──────────────────────────────────────────┤ -│ SIDEBAR │ CONTENT AREA │ -│ ▼ Recettes │ 📄 Titre du fichier │ -│ 📁 Soupes │ Tags: #recette #rapide │ -│ 📄 Pizza │ [Contenu Markdown rendu] │ -│ ▼ IT │ │ -│ 📁 Docker │ │ -│ Tags Cloud │ │ -└──────────────┴──────────────────────────────────────────┘ -``` +![Interface ObsiGate — tableau de bord Statistiques avec vaults, tags et raccourcis clavier](docs/images/obsigate-home.png) + +> Interface web d'ObsiGate : sidebar multi-vault, recherche globale, statistiques et raccourcis. + +--- + +## 📚 Guides + +Les **guides d'utilisation** pas à pas se trouvent dans [`docs/GUIDES/`](docs/GUIDES/) : + +| Guide | Contenu | +|---|---| +| 🚀 [Prise en main](docs/GUIDES/PRISE_EN_MAIN.md) | Premier lancement, interface, navigation, vaults, raccourcis | +| 🔍 [Recherche, PDF & Excalidraw](docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md) | Syntaxe de requête, recherche sémantique, lecteur PDF, diagrammes | +| 🤖 [Assistant IA & Forge](docs/GUIDES/ASSISTANT_IA_FORGE.md) | Fournisseurs, éditeur IA, BooksLM, Forge, commandes `@` / `/` | +| 📝 [Édition & collaboration](docs/GUIDES/COLLABORATION.md) | Édition simultanée, curseurs distants, persistance | +| 📱 [PWA & hors-ligne](docs/GUIDES/PWA_HORS_LIGNE.md) | Installation, cache hors-ligne, file de synchro, notifications | +| 🔌 [API REST](docs/GUIDES/API_REST.md) | Authentification, clés API, endpoints, exemples `curl`, SSE | +| 🧩 [Serveur MCP](docs/GUIDES/MCP.md) | Brancher Claude Desktop, Cursor, Cline… sur vos vaults | +| 🔒 [Authentification & sécurité](docs/GUIDES/AUTHENTIFICATION_SECURITE.md) | Utilisateurs, MFA, permissions par vault, durcissement | +| 🐳 [Déploiement Docker](docs/GUIDES/DEPLOIEMENT_DOCKER.md) | `docker-compose`, volumes, reverse proxy, mises à jour | +| 🖥️ [Desktop (Tauri)](docs/GUIDES/DESKTOP.md) | Installation, premier lancement, build depuis les sources, dépannage | + +> Index complet : [`docs/GUIDES/README.md`](docs/GUIDES/README.md). --- ## 📋 Table des matières -- [Fonctionnalités](#fonctionnalites) -- [Prérequis](#prerequis) -- [Installation rapide](#installation-rapide) -- [Configuration détaillée](#configuration-detaillee) -- [Variables d'environnement](#variables-denvironnement) -- [🔒 Authentification](#authentification) -- [Ajouter une nouvelle vault](#ajouter-une-nouvelle-vault) -- [Build & déploiement avec build.sh](#build-deploiement-avec-buildsh) -- [Rendu d'images Obsidian](#rendu-dimages-obsidian) -- [Desktop (Tauri) — Application native](#desktop-tauri-application-native) -- [Utilisation](#utilisation) -- [API](#api) -- [Recherche avancée](#recherche-avancee) -- [Dépannage](#depannage) -- [Performance](#performance) -- [Sécurité](#securite) -- [Stack technique](#stack-technique) -- [Architecture](#architecture) -- [Développement](#developpement) -- [Licence](#licence) -- [Changelog](#changelog) +- ✨ [Fonctionnalités](#fonctionnalites) +- 📚 [Guides](#guides) +- 🚀 [Prérequis](#prerequis) +- ⚡ [Installation rapide](#installation-rapide) +- ⚙️ [Configuration détaillée](#configuration-detaillee) +- 🌍 [Variables d'environnement](#variables-denvironnement) +- 🔒 [Authentification](#authentification) +- ➕ [Ajouter une nouvelle vault](#ajouter-une-nouvelle-vault) +- 🔨 [Build & déploiement avec build.sh](#build-deploiement-avec-buildsh) +- 🖼️ [Rendu d'images Obsidian](#rendu-dimages-obsidian) +- 🖥️ [Desktop (Tauri) — Application native](#desktop-tauri-application-native) +- 📖 [Utilisation](#utilisation) +- 👥 [Collaboration temps réel](#collaboration-temps-reel) +- 🔌 [API](#api) +- 🔍 [Recherche avancée](#recherche-avancee) +- 🔧 [Dépannage](#depannage) +- ⚡ [Performance](#performance) +- 🛡️ [Sécurité](#securite) +- 🏗️ [Stack technique](#stack-technique) +- 🏠 [Architecture](#architecture) +- 📝 [Développement](#developpement) +- 📄 [Licence](#licence) +- 🤝 [Support](#support) +- 📝 [Changelog](#changelog) --- ## ✨ Fonctionnalités - **🤖 AI Editor intégré** — Éditeur CodeMirror 6 avec toolbar IA : amélioration, correction, traduction, génération, réécriture personnalisée, toolbox (liste, tableau, frontmatter, canvas) — multi-provider DeepSeek/OpenRouter/Gemini -- **🧩 Serveur MCP & agent IA** — Serveur Model Context Protocol intégré (`/mcp`) et assistant avec function calling : lisez, cherchez et modifiez vos vaults depuis Claude Desktop, Cursor… avec confirmations two-step, permissions par vault, rate limiting et redaction des secrets ([guide](docs/MCP_GUIDE.md)) +- **🧩 Serveur MCP & agent IA** — Serveur Model Context Protocol intégré (`/mcp`) et assistant avec function calling : lisez, cherchez et modifiez vos vaults depuis Claude Desktop, Cursor… avec confirmations two-step, permissions par vault, rate limiting et redaction des secrets ([guide](docs/GUIDES/MCP.md)) - **👥 Collaboration temps réel** — Édition simultanée d'un même document (Yjs/CRDT) : curseurs distants colorés, indicateur de présence, fusion sans conflit, reconnexion automatique et persistance serveur ([détail](docs/features/collaboration.md)) - **📖 Guide d'utilisation intégré** — Aide complète en FR/EN accessible depuis le menu Options : interface, navigation, recherche, fichiers, IA, sécurité, API & intégrations (OpenAPI, MCP), hors-ligne, collaboration, desktop, plus une section **Architecture** avec diagramme Mermaid ; téléchargeable en **Markdown** et **PDF** dans la langue courante ([détail](docs/features/guide-coverage-105.md)) - **📱 Éditeur mobile natif** — Édition optimisée pour le tactile : barre d'outils Markdown flottante (gras/italique/code/liste/lien), bouton « Coller » persistant (contournement iOS), zoom par pincement et hauteur ajustable, raccourcis swipe (liens entrants / table des matières) et mode lecture plein écran avec navigation entre fichiers ([détail](docs/features/mobile-editor.md)) @@ -412,6 +426,8 @@ curl -X POST http://localhost:2020/api/attachments/rescan/MonVault ## 🖥️ Desktop (Tauri) — Application native +> 📖 Guide complet : [Desktop (Tauri)](docs/GUIDES/DESKTOP.md) + ObsiGate Desktop est une application native construite avec [Tauri](https://tauri.app/) (Rust + webview système). Elle embarque le backend Python et le frontend dans un exécutable standalone — zéro Docker, zéro ligne de commande. > 🚧 **Version 2.0.0 — binaires en cours de stabilisation.** Pour l'instant, le build depuis les sources est recommandé. @@ -571,6 +587,8 @@ Cycle de vie : Tauri spawn le backend Python → health check → splash de dém ## 👥 Collaboration temps réel +> 📖 Guide complet : [Édition & collaboration](docs/GUIDES/COLLABORATION.md) + Plusieurs utilisateurs peuvent éditer le même document markdown simultanément (façon Google Docs) : - **Fusion sans conflit** grâce à Yjs (CRDT) : deux personnes peuvent taper au même endroit, aucune @@ -590,6 +608,8 @@ fenêtres) pour voir la collaboration en action. ## 🔌 API +> 📖 Guide complet : [API REST](docs/GUIDES/API_REST.md) · [Serveur MCP](docs/GUIDES/MCP.md) + ObsiGate expose une API REST complète : | Endpoint | Description | Méthode | Auth | @@ -637,6 +657,8 @@ curl "http://localhost:2020/api/file/Recettes?path=pizza.md" ## 🔍 Recherche avancée +> 📖 Guide complet : [Recherche, PDF & Excalidraw](docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md) + ### Syntaxe de requête | Opérateur | Description | Exemple | @@ -758,6 +780,8 @@ Configurables via l'interface (Settings) ou l'API `/api/config`. ## 🛡️ Sécurité +> 📖 Guide complet : [Authentification & sécurité](docs/GUIDES/AUTHENTIFICATION_SECURITE.md) + - **Path traversal** : tous les endpoints fichier valident que le chemin résolu reste dans la vault - **Rate limiting** : 10 tentatives de login max par IP sur 15 minutes + lockout par compte (5 tentatives) - **Audit log** : écritures/suppressions/config journalisées dans `data/audit.log` (JSON lines, rotation 10 MB) @@ -927,8 +951,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l ## 📝 Changelog -Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.16.5). +Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.16.6). --- -*Projet : ObsiGate | Version : 2.16.5 | Dernière mise à jour : Juin 2026* +*Projet : ObsiGate | Version : 2.16.6 | Dernière mise à jour : Septembre 2026* diff --git a/README.md b/README.md index cde5eeb..af62fae 100644 --- a/README.md +++ b/README.md @@ -2,53 +2,73 @@ **Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface. -[![Version](https://img.shields.io/badge/Version-2.16.5-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.16.6-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) [![CI/CD](https://img.shields.io/badge/CI%2FCD-Gitea_Actions-green.svg)](https://git.dracodev.net/Projets/ObsiGate/actions) -``` -┌─────────────────────────────────────────────────────────┐ -│ [🔍 Search...] [☀/🌙 Theme] ObsiGate │ -├──────────────┬──────────────────────────────────────────┤ -│ SIDEBAR │ CONTENT AREA │ -│ ▼ Recipes │ 📄 File Title │ -│ 📁 Soups │ Tags: #recipe #quick │ -│ 📄 Pizza │ [Rendered Markdown Content] │ -│ ▼ IT │ │ -│ 📁 Docker │ │ -│ Tags Cloud │ │ -└──────────────┴──────────────────────────────────────────┘ -``` +![ObsiGate interface — statistics dashboard with vaults, tags and keyboard shortcuts](docs/images/obsigate-home.png) + +> ObsiGate web interface: multi-vault sidebar, global search, dashboard stats and shortcuts. + +--- + +## 📚 Guides + +Step-by-step **user guides** live in [`docs/GUIDES/`](docs/GUIDES/): + +| Guide | What it covers | +|---|---| +| 🚀 [Getting Started](docs/GUIDES/PRISE_EN_MAIN.md) | First run, interface, navigation, vaults, shortcuts | +| 🔍 [Search, PDF & Excalidraw](docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md) | Query syntax, semantic search, PDF viewer, diagrams | +| 🤖 [AI Assistant & Forge](docs/GUIDES/ASSISTANT_IA_FORGE.md) | Providers, AI editor, BooksLM, Forge, `@` / `/` commands | +| 📝 [Editing & Collaboration](docs/GUIDES/COLLABORATION.md) | Simultaneous editing, remote cursors, persistence | +| 📱 [PWA & Offline](docs/GUIDES/PWA_HORS_LIGNE.md) | Install as an app, offline cache, sync queue, push | +| 🔌 [REST API](docs/GUIDES/API_REST.md) | Authentication, API keys, endpoints, `curl` examples, SSE | +| 🧩 [MCP Server](docs/GUIDES/MCP.md) | Connect Claude Desktop, Cursor, Cline… to your vaults | +| 🔒 [Auth & Security](docs/GUIDES/AUTHENTIFICATION_SECURITE.md) | Users, MFA, per-vault permissions, hardening | +| 🐳 [Docker Deployment](docs/GUIDES/DEPLOIEMENT_DOCKER.md) | `docker-compose`, volumes, reverse proxy, updates | +| 🖥️ [Desktop (Tauri)](docs/GUIDES/DESKTOP.md) | Install, first run, build from source, troubleshooting | + +> All guides are currently written in **French**. See the full index: +> [`docs/GUIDES/README.md`](docs/GUIDES/README.md). --- ## 📋 Table of Contents -- [Features](#features) -- [Architecture](#architecture) -- [Prerequisites](#prerequisites) -- [Quick Installation](#quick-installation) -- [Detailed Configuration](#detailed-configuration) -- [Environment Variables](#environment-variables) -- [🔒 Authentication](#authentication) -- [Adding a New Vault](#adding-a-new-vault) -- [Build & Deployment with build.sh](#build--deployment-with-buildsh) -- [Desktop (Tauri) — Native Application](#desktop-tauri--native-application) -- [Usage](#usage) -- [API](#api) -- [Performance](#performance) -- [Troubleshooting](#troubleshooting) -- [Tech Stack](#tech-stack) -- [Changelog](#changelog) +- ✨ [Features](#features) +- 📚 [Guides](#guides) +- 🚀 [Prerequisites](#prerequisites) +- ⚡ [Quick Installation](#quick-installation) +- ⚙️ [Detailed Configuration](#detailed-configuration) +- 🌍 [Environment Variables](#environment-variables) +- 🔒 [Authentication](#authentication) +- ➕ [Adding a New Vault](#adding-a-new-vault) +- 🔨 [Build & Deployment with build.sh](#build--deployment-with-buildsh) +- 🖼️ [Obsidian Image Rendering](#obsidian-image-rendering) +- 🖥️ [Desktop (Tauri) — Native Application](#desktop-tauri--native-application) +- 📖 [Usage](#usage) +- 👥 [Real-time Collaboration](#real-time-collaboration) +- 🔌 [API](#api) +- 🔍 [Advanced Search](#advanced-search) +- 🛡️ [Security](#security) +- ⚡ [Performance](#performance) +- 🔧 [Troubleshooting](#troubleshooting) +- 🏗️ [Tech Stack](#tech-stack) +- 🏠 [Architecture](#architecture) +- 📝 [Development](#development) +- 📄 [License](#license) +- 🤝 [Support](#support) +- 📝 [Changelog](#changelog) --- ## ✨ Features - **🤖 Integrated AI Editor** — CodeMirror 6 editor with AI toolbar: improve, correct, translate, generate, custom rewrite, toolbox (list, table, frontmatter, canvas) — multi-provider DeepSeek/OpenRouter/Gemini -- **🧩 MCP Server & AI Agent** — Built-in Model Context Protocol server (`/mcp`) and tool-calling assistant: read, search and edit your vaults from Claude Desktop, Cursor… with two-step confirmations, per-vault permissions, rate limiting and secret redaction ([guide](docs/MCP_GUIDE.md)) +- **🧩 MCP Server & AI Agent** — Built-in Model Context Protocol server (`/mcp`) and tool-calling assistant: read, search and edit your vaults from Claude Desktop, Cursor… with two-step confirmations, per-vault permissions, rate limiting and secret redaction ([guide](docs/GUIDES/MCP.md)) - **👥 Real-time Collaboration** — Simultaneous editing of the same document (Yjs/CRDT): colored remote cursors, presence indicator, conflict-free merge, automatic reconnection and server-side persistence ([details](docs/features/collaboration.md)) - **📖 Built-in User Guide** — Complete FR/EN help from the Options menu: interface, navigation, search, files, AI, security, API & integrations (OpenAPI, MCP), offline, collaboration, desktop, plus an **Architecture** section with a Mermaid diagram; downloadable as **Markdown** and **PDF** in the current language ([details](docs/features/guide-coverage-105.md)) - **📱 Native Mobile Editor** — Touch-optimised editing: floating Markdown toolbar (bold/italic/code/list/link), persistent Paste button (iOS workaround), pinch-zoom font & adjustable height, swipe shortcuts (backlinks / table of contents) and a full-screen reading mode with page navigation ([details](docs/features/mobile-editor.md)) @@ -520,6 +540,8 @@ curl -X POST http://localhost:2020/api/attachments/rescan/MyVault ## 🖥️ Desktop (Tauri) — Native Application +> 📖 Full guide: [Desktop (Tauri)](docs/GUIDES/DESKTOP.md) + ObsiGate Desktop is a native application built with [Tauri](https://tauri.app/) (Rust + system webview). It embeds the Python backend and frontend in a standalone executable — zero Docker, zero command line. > 🚧 **Version 2.0.0 — binaries are being stabilized.** For now, building from source is recommended. @@ -687,6 +709,8 @@ Lifecycle: Tauri spawns the Python backend → health check → opens the webvie ## 👥 Real-time Collaboration +> 📖 Full guide: [Editing & Collaboration](docs/GUIDES/COLLABORATION.md) + Multiple users can edit the same markdown document simultaneously (Google Docs style): - **Conflict-free merge** via Yjs (CRDT): two people can type in the same place, no change is lost. @@ -703,6 +727,8 @@ No configuration is required: open the same file in two browsers (or two windows ## 🔌 API +> 📖 Full guide: [REST API](docs/GUIDES/API_REST.md) · [MCP Server](docs/GUIDES/MCP.md) + ObsiGate exposes a complete REST API : | Endpoint | Description | Method | Auth | @@ -763,6 +789,8 @@ curl "http://localhost:2020/api/file/Recipes?path=pizza.md" ## 🔍 Advanced Search +> 📖 Full guide: [Search, PDF & Excalidraw](docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md) + ### Query Syntax | Operator | Description | Example | @@ -915,6 +943,8 @@ These parameters are configurable via the interface (Settings) or the `/api/conf ## 🛡️ Security +> 📖 Full guide: [Auth & Security](docs/GUIDES/AUTHENTIFICATION_SECURITE.md) + - **Path traversal** : All file endpoints validate that the resolved path stays within the vault - **Rate limiting** : 10 login attempts max per IP over 15 minutes + per-account lockout (5 attempts) - **Audit log** : All writes, deletions, and config changes are logged in `data/audit.log` (JSON lines, 10 MB rotation) @@ -1070,7 +1100,9 @@ ObsiGate/ ├── Dockerfile # Multi-stage, healthcheck, non-root ├── docker-compose.yml # Deployment with healthcheck and auth env vars ├── build.sh # Automated build & deployment (docker compose build + up) -└── docs/CONTRIBUTING.md # Contribution guide +└── docs/ + ├── GUIDES/ # User guides (getting started, API, MCP, desktop…) + └── CONTRIBUTING.md # Contribution guide ``` ### Contributing @@ -1096,8 +1128,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) ## 📝 Changelog -See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.16.5). +See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.16.6). --- -*Project: ObsiGate | Version: 2.16.5 | Last updated: May 2026* +*Project: ObsiGate | Version: 2.16.6 | Last updated: September 2026* diff --git a/VERSION b/VERSION index ffa5fee..3c5d010 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.16.5 +2.16.6 diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index 6db9bf0..bd4e201 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.16.5" +version = "2.16.6" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index a902024..1bb7f8f 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.16.5" +version = "2.16.6" description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian" authors = ["Bruno Charest"] edition = "2021" diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index be0eed7..5caef42 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json", "productName": "ObsiGate", - "version": "2.16.5", + "version": "2.16.6", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/AI_ARCHITECTURE_GUIDE.md b/docs/AI_ARCHITECTURE_GUIDE.md index 98556c0..eec8cad 100644 --- a/docs/AI_ARCHITECTURE_GUIDE.md +++ b/docs/AI_ARCHITECTURE_GUIDE.md @@ -347,7 +347,7 @@ Pour répondre au besoin de cibler un fournisseur/modèle sans dépendre uniquem | **0 — Fondations** | `backend/tools/` (registry, context, service, audit) + extraction des services métier + tests unitaires | Couche d'outils testable sans IA | | **1 — Function calling in-app** | Abstraction tool-calling multi-provider, agent loop, confirmations UI, SSE réel, outils de navigation | Assistant qui lit/cherche/lit/ouvre/modifie avec confirmation | | **2 — Serveur MCP** | `backend/mcp/server.py` (tools + resources + prompts), **Streamable HTTP** (`/mcp`, auth JWT), confirmation two-step | ObsiGate accessible comme serveur MCP (local + distant, multi-utilisateur) | -| **3 — Durcissement** ✅ | Rate limiting (`backend/tools/ratelimit.py`), quotas `BOOKSLM_MAX_*`, redaction systématique des résultats (`backend/tools/redaction.py`), doc OpenAPI (tag/path MCP) + [guide MCP](./MCP_GUIDE.md), tests E2E | Observabilité et sécurité complètes | +| **3 — Durcissement** ✅ | Rate limiting (`backend/tools/ratelimit.py`), quotas `BOOKSLM_MAX_*`, redaction systématique des résultats (`backend/tools/redaction.py`), doc OpenAPI (tag/path MCP) + [guide MCP](./GUIDES/MCP.md), tests E2E | Observabilité et sécurité complètes | Voir `docs/ROADMAP.md` (item dédié) pour le détail des activités. @@ -380,7 +380,7 @@ Voir `docs/ROADMAP.md` (item dédié) pour le détail des activités. - `backend/mcp/confirmations.py` — jetons de confirmation signés (two-step, anti-rejeu) - `backend/tools/ratelimit.py` — rate limiting par jeton/outil (phase F) - `backend/tools/redaction.py` — redaction récursive des résultats d'outils (phase F) -- `docs/MCP_GUIDE.md` — guide d'installation et d'utilisation des clients MCP +- `docs/GUIDES/MCP.md` — guide d'installation et d'utilisation des clients MCP - `backend/bookslm.py`, `backend/bookslm_routes.py` — assistant contextuel (+ endpoint `/agent`) - `frontend/js/ai.js`, `frontend/js/bookslm.js` — UI IA - `backend/auth/middleware.py` — permissions diff --git a/docs/GUIDES/API_REST.md b/docs/GUIDES/API_REST.md new file mode 100644 index 0000000..88292da --- /dev/null +++ b/docs/GUIDES/API_REST.md @@ -0,0 +1,326 @@ +# 🔌 Guide de l'API REST + +ObsiGate expose une **API REST complète** couvrant toute l'application : +vaults, fichiers, recherche, sauvegardes, exports, IA, partage, webhooks et +administration. Ce guide explique l'authentification, la création de clés et +donne des exemples prêts à l'emploi. + +> **Public :** développeurs, intégrateurs, scripts d'automatisation +> **Doc interactive :** `/docs` (Swagger UI) · `/redoc` (ReDoc) · `/openapi.json` +> **Voir aussi :** [Serveur MCP](./MCP.md) · [Authentification & sécurité](./AUTHENTIFICATION_SECURITE.md) + +--- + +## 1. Base et conventions + +| Élément | Valeur | +|---|---| +| URL de base | `http://:2020` (Docker) ou `http://127.0.0.1:17890` (desktop) | +| Préfixe API | `/api` | +| Format | JSON (`application/json`) | +| Version | suit la version d'ObsiGate (header `X-…`, `/api/health`) | +| Erreurs | `{"detail": "..."}` + code HTTP (`400`, `401`, `403`, `404`, `409`, `422`, `500`) | + +Quand l'authentification est **désactivée** (`OBSIGATE_AUTH_ENABLED=false`), tous +les endpoints sont accessibles sans jeton (utilisateur anonyme avec accès à tous +les vaults). + +--- + +## 2. Authentification + +### 2.1 Jeton de session (JWT) + +Obtenu via `POST /api/auth/login`. Le jeton d'accès a une durée de vie courte +(`OBSIGATE_ACCESS_TOKEN_TTL`, défaut 3600 s) et un refresh token longue durée est +posé en cookie HTTP-only. + +```bash +curl -s -X POST http://localhost:2020/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"votre_mot_de_passe"}' +``` + +Réponse (extrait) : + +```json +{ + "access_token": "eyJ...", + "token_type": "bearer", + "expires_in": 3600, + "user": { "username": "admin", "role": "admin", "vaults": ["*"] } +} +``` + +Deux façons de présenter le jeton : + +```http +Authorization: Bearer +``` + +ou, pour un client navigateur, le cookie HTTP-only avec +`credentials: "include"` (le login pose aussi un cookie `access_token`). + +### 2.2 Clés API longue durée (recommandé pour scripts & MCP) + +Une **seule clé** authentifie **l'API REST et le serveur MCP**. Créez-la depuis +l'interface (Configurations → **🔑 Clés API & MCP**) ou par API : + +```bash +# 1. Se connecter, récupérer le token (section 2.1) +# 2. Créer une clé valable 30 jours +curl -s -X POST http://localhost:2020/api/auth/tokens \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"name":"Script backup","expiry":"30d"}' +``` + +Réponse (`token` affiché **une seule fois**) : + +```json +{ + "token": "eyJ...", + "jti": "…", + "name": "Script backup", + "created_at": 1790000000, + "expires_at": 1792592000, + "expiry_key": "30d" +} +``` + +| `expiry` | Durée | +|---|---| +| `1d` | 1 jour | +| `30d` | 1 mois | +| `180d` | 6 mois | +| `365d` | 1 an | +| `never` | sans expiration | + +Gestion : + +| Endpoint | Rôle | +|---|---| +| `GET /api/auth/tokens` | Lister vos clés (`last_used_at`, statut) | +| `POST /api/auth/tokens` | Créer (`{name, expiry}`) | +| `DELETE /api/auth/tokens/{jti}` | Révoquer immédiatement (API **et** MCP) | + +> Le JWT brut n'est **jamais persisté** : copiez-le à la création. Plafond : +> 50 clés actives par utilisateur. + +--- + +## 3. Référence des endpoints + +> Liste non exhaustive — la référence faisant foi est `/openapi.json`. Les +> colonnes **Auth** indiquent le niveau requis (`—`, `Oui`, `Admin`). + +### 3.1 Système + +| Endpoint | Description | Méthode | Auth | +|---|---|---|---| +| `/api/health` | Santé (statut, version, stats) | GET | — | +| `/api/health/detailed` | Santé détaillée | GET | — | +| `/api/config` | Lire / écrire la configuration | GET/POST | Oui/Admin | +| `/api/diagnostics` | Statistiques index & mémoire | GET | Admin | +| `/api/dashboard` | Statistiques du tableau de bord | GET | Oui | +| `/api/events` | Flux SSE temps réel | GET | Oui | + +### 3.2 Vaults + +| Endpoint | Description | Méthode | Auth | +|---|---|---|---| +| `/api/vaults` | Liste (filtrée par permissions) | GET | Oui | +| `/api/vaults/status` | Statut de toutes les vaults | GET | Oui | +| `/api/vaults/add` | Ajouter une vault (volume déjà monté) | POST | Admin | +| `/api/vaults/{name}` | Supprimer une vault | DELETE | Admin | +| `/api/index/reload` | Réindexation complète | GET | Admin | +| `/api/index/reload/{vault}` | Réindexer une vault | GET | Oui | +| `/api/vaults/{vault}/settings` | Lire / écrire les réglages | GET/POST | Oui | +| `/api/attachments/rescan/{vault}` | Rescanner les attachements | POST | Oui | + +### 3.3 Fichiers + +| Endpoint | Description | Méthode | Auth | +|---|---|---|---| +| `/api/browse/{vault}?path=` | Naviguer dans les dossiers | GET | Oui | +| `/api/file/{vault}?path=` | Contenu rendu (Markdown) | GET | Oui | +| `/api/file/{vault}/raw?path=` | Contenu brut | GET | Oui | +| `/api/file/{vault}/download?path=` | Télécharger | GET | Oui | +| `/api/file/{vault}/save?path=` | Enregistrer | PUT | Oui | +| `/api/file/{vault}` | Créer | POST | Oui | +| `/api/file/{vault}` | Renommer | PATCH | Oui | +| `/api/file/{vault}` | Supprimer | DELETE | Oui | +| `/api/directory/{vault}` | Créer / renommer / supprimer un dossier | POST/PATCH/DELETE | Oui | +| `/api/move/{vault}` | Déplacer un fichier/dossier | POST | Oui | +| `/api/vault/{vault}/batch-upload` | Upload multiple (multipart) | POST | Oui | +| `/api/image/{vault}?path=` | Servir une image | GET | Oui | + +### 3.4 Recherche & graphe + +| Endpoint | Description | Méthode | Auth | +|---|---|---|---| +| `/api/search` | Recherche simple (legacy) | GET | Oui | +| `/api/search/advanced` | Recherche TF-IDF avancée (facettes, tri, pagination, `semantic=`) | GET | Oui | +| `/api/search/replace` | Recherche/remplacement multi-fichiers | POST | Oui | +| `/api/tags?vault=` | Tags uniques avec compteurs | GET | Oui | +| `/api/suggest?q=` | Autocomplétion de titres | GET | Oui | +| `/api/tags/suggest?q=` | Autocomplétion de tags | GET | Oui | +| `/api/tree-search` | Recherche de fichiers/dossiers | GET | Oui | +| `/api/vault/{vault}/paths` | Liste de chemins | GET | Oui | +| `/api/graph/{vault}` | Graphe de liens | GET | Oui | + +### 3.5 Sauvegardes + +| Endpoint | Description | Méthode | Auth | +|---|---|---|---| +| `/api/file/{vault}/backups` | Backups d'un fichier | GET | Oui | +| `/api/file/{vault}/diff` | Diff avec une version | GET | Oui | +| `/api/file/{vault}/restore` | Restaurer une version | POST | Oui | +| `/api/backups` | Lister les backups | GET | Oui | +| `/api/backups/content` | Contenu d'un backup | GET | Oui | +| `/api/backups/delete` / `/purge` / `/compress` / `/auto` | Gestion & purge | POST | Oui | + +### 3.6 Exports + +| Endpoint | Description | Méthode | +|---|---|---| +| `/api/export/html` | Exporter en HTML | GET | +| `/api/export/md-bundle` | Exporter en bundle Markdown (ZIP) | GET | +| `/api/export/epub` | Exporter en ePub | GET | +| `/api/guide/download?format=md\|pdf&lang=fr\|en` | Télécharger le guide intégré | GET | + +### 3.7 PDF + +| Endpoint | Description | Méthode | +|---|---|---| +| `/api/file/{vault}/pdf/info` | Métadonnées sans transfert | GET | +| `/api/file/{vault}/pdf/stream` | Streaming (HTTP Range, 206) | GET | + +### 3.8 IA + +| Endpoint | Description | Méthode | +|---|---|---| +| `/api/ai/status` | Statut des fournisseurs | GET | +| `/api/ai/improve`, `/fix-spelling`, `/summarize`, `/translate`, `/rewrite`, `/to-list`, `/to-table`, `/frontmatter`, `/inline-complete`, `/to-canvas`… | Actions éditeur IA | POST | +| `/api/ai/model-capabilities?provider=&model=` | Capacités d'un modèle | GET | +| `/api/ai/bookslm/*` | Console IA par répertoire | POST/GET | +| `/api/ai/skills` | Lister / créer / supprimer des skills | GET/POST/DELETE | +| `/api/config/ai-keys` · `/api/config/tool-keys` | Clés fournisseurs & sources | GET/POST/DELETE | + +### 3.9 Authentification & administration + +| Endpoint | Description | Méthode | Auth | +|---|---|---|---| +| `/api/auth/status` | Statut de l'auth | GET | — | +| `/api/auth/login` · `/refresh` · `/logout` | Cycle de session | POST | — / Cookie / Oui | +| `/api/auth/me` | Profil courant | GET/PATCH | Oui | +| `/api/auth/change-password` | Changer le mot de passe | POST | Oui | +| `/api/auth/mfa/*` | TOTP, WebAuthn, recovery | POST/GET | Oui | +| `/api/auth/tokens` | Clés API (voir §2.2) | GET/POST/DELETE | Oui | +| `/api/auth/admin/users` | Lister / créer des utilisateurs | GET/POST | Admin | +| `/api/auth/admin/users/{u}` | Modifier / supprimer | PATCH/DELETE | Admin | +| `/api/admin/stats` · `/audit` · `/backup-stats` · `/stream` | Monitoring admin | GET | Admin | + +### 3.10 Partage, webhooks, conflits, plugins, push + +| Endpoint | Description | Méthode | +|---|---|---| +| `/api/share/{vault}` | Créer un lien de partage public | POST | +| `/api/shares` | Lister / supprimer les partages | GET/DELETE | +| `/api/webhooks` | CRUD webhooks (HMAC-SHA256) | GET/POST/PATCH/DELETE | +| `/api/conflicts` · `/api/conflicts/resolve` | Conflits Syncthing | GET/POST | +| `/api/plugins` | Installer / activer / désactiver | GET/POST/DELETE | +| `/api/push/*` | Abonnement Web Push (VAPID) | GET/POST/DELETE | + +--- + +## 4. Exemples `curl` + +```bash +BASE=http://localhost:2020 +TOKEN=$(curl -s -X POST $BASE/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"secret"}' | jq -r .access_token) + +# Santé +curl -s $BASE/api/health + +# Lister les vaults +curl -s $BASE/api/vaults -H "Authorization: Bearer $TOKEN" + +# Naviguer +curl -s "$BASE/api/browse/Recettes?path=" -H "Authorization: Bearer $TOKEN" + +# Lire un fichier (rendu Markdown) +curl -s "$BASE/api/file/Recettes?path=pizza.md" -H "Authorization: Bearer $TOKEN" + +# Lire en brut +curl -s "$BASE/api/file/Recettes/raw?path=pizza.md" -H "Authorization: Bearer $TOKEN" + +# Sauvegarder +curl -s -X PUT "$BASE/api/file/Recettes/save?path=pizza.md" \ + -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ + -d '{"content":"# Pizza\n\nNouvelle recette."}' + +# Recherche avancée +curl -s "$BASE/api/search/advanced?q=tag:cuisine%20pizza&vault=all&limit=20&offset=0&sort=relevance" \ + -H "Authorization: Bearer $TOKEN" + +# Autocomplétion +curl -s "$BASE/api/suggest?q=piz&vault=all" -H "Authorization: Bearer $TOKEN" + +# Forcer une réindexation +curl -s $BASE/api/index/reload -H "Authorization: Bearer $TOKEN" +``` + +> Le mot de passe peut aussi être fourni par une clé API dans `Authorization`. +> Quand l'auth est désactivée, omettez l'en-tête. + +--- + +## 5. Temps réel + +### 5.1 SSE — `/api/events` + +Flux d'événements de changement d'index (fichiers créés/supprimés/modifiés), avec +reconnexion automatique côté client. + +```bash +curl -N "$BASE/api/events" +``` + +### 5.2 WebSocket — collaboration + +`ws(s):///ws/collab/{vault}/{path}` transporte les mises à jour +Yjs/CRDT et la présence (curseurs distants). Authentification par cookie +`access_token` ou paramètre `?token=`, avec contrôle d'accès par vault. +Voir [Édition & collaboration](./COLLABORATION.md). + +--- + +## 6. Limites et bonnes pratiques + +- **Rate limiting** : les endpoints de login et les outils IA sont limités ; + respectez `retry_after` en cas de `429`. +- **Permissions** : chaque endpoint fichier vérifie l'accès au vault et rejette + les chemins hors vault (path traversal). +- **Clés API** : préférez-les aux mots de passe pour les scripts ; révoquez-les + dès qu'elles ne servent plus. +- **Gros volumes** : utilisez la pagination (`limit`/`offset`) et le streaming + HTTP Range pour les PDF. +- **Exports** : `md-bundle` et `epub` renvoient un fichier binaire — utilisez + `-o` avec `curl`. + +--- + +## 7. Dépannage + +| Code | Cause probable | +|---|---| +| `401` | Jeton absent, expiré ou révoqué | +| `403` | Compte sans accès à cette vault / réservé admin | +| `404` | Vault, fichier ou chemin inexistant | +| `409` | Conflit (fichier déjà existant, etc.) | +| `422` | Corps de requête invalide (schéma Pydantic) | +| `429` | Rate limit dépassé — voir `retry_after` | +| `501` | Export PDF indisponible (WeasyPrint/GTK absent) | diff --git a/docs/GUIDES/ASSISTANT_IA_FORGE.md b/docs/GUIDES/ASSISTANT_IA_FORGE.md new file mode 100644 index 0000000..479be6e --- /dev/null +++ b/docs/GUIDES/ASSISTANT_IA_FORGE.md @@ -0,0 +1,217 @@ +# 🤖 Guide Assistant IA & Forge + +ObsiGate intègre un **assistant IA** capable de lire, rechercher et modifier vos +notes, ainsi qu'un **éditeur IA** (CodeMirror + toolbar) et une console +contextuelle par répertoire (**BooksLM**). Ce guide explique comment les +configurer et les utiliser. + +> **Fiches techniques :** [`ai-tools-mcp.md`](../features/ai-tools-mcp.md) · +> [`ai-assistant-commands.md`](../features/ai-assistant-commands.md) · +> [`ai-quick-actions.md`](../features/ai-quick-actions.md) · +> [`forge-assistant.md`](../features/forge-assistant.md) · +> [`bookslm.md`](../features/bookslm.md) · +> [`ai-tools-roadmap.md`](../features/ai-tools-roadmap.md) +> **Voir aussi :** [Serveur MCP](./MCP.md) · [API REST](./API_REST.md) + +--- + +## 1. Vue d'ensemble + +L'IA d'ObsiGate se compose de plusieurs surfaces complémentaires : + +| Surface | Rôle | +|---|---| +| **Éditeur IA** | Toolbar d'actions sur le document ouvert (CodeMirror) | +| **Assistant IA** | Panneau de discussion avec *function calling* sur vos vaults | +| **BooksLM** | Console IA contextuelle sur un **répertoire** (style NotebookLM) | +| **Forge** | Éditeur avancé avec assistant IA intégré | +| **Outils (tools)** | Lecture, recherche, écriture, opérations destructives (two-step) | +| **MCP** | Exposition des mêmes outils à Claude Desktop, Cursor, Cline… | + +--- + +## 2. Configurer un fournisseur + +### 2.1 Fournisseurs supportés + +ObsiGate est **multi-fournisseur** : + +- **DeepSeek** +- **OpenRouter** +- **Google Gemini** + +Chaque fournisseur se configure au choix : + +1. **Depuis l'interface** — menu → Configurations → **Clés API IA**. La clé saisie + est stockée dans `data/api_keys.json` et **prime** sur la variable + d'environnement. +2. **Par variable d'environnement** — voir `.env.example`. + +### 2.2 Modèle et capacités + +L'interface affiche les **capacités** de chaque modèle (8 indicateurs : vision, +tool calling, contexte long, etc.), via +`GET /api/ai/model-capabilities?provider=&model=`. Le picker de l'assistant +propose une recherche de modèle et une bulle d'information ⓘ. + +Vous pouvez définir un **modèle par défaut** et un fournisseur par défaut dans la +configuration. Le fournisseur/modèle est **partagé** entre l'assistant et Forge. + +### 2.3 Tester la configuration + +`POST /api/config/ai-keys/test` vérifie qu'une clé fonctionne. En cas d'échec, +un message explicite s'affiche. + +--- + +## 3. Éditeur IA (toolbar) + +Quand un document Markdown est ouvert dans l'éditeur, une **toolbar IA** propose +des actions qui remplacent ou insèrent du contenu. Actions principales : + +| Action | Effet | +|---|---| +| **Améliorer** | Relecture et amélioration générale | +| **Corriger** | Correction orthographique et grammaticale | +| **Raccourcir / Allonger** | Ajuste la longueur du texte | +| **Simplifier** | Vulgarise le contenu | +| **Ton** | Adapte le registre (formel, neutre…) | +| **Traduire** | Traduit la sélection ou le document | +| **Expliquer** | Explique un passage | +| **Résumer** | Produit un résumé | +| **Continuer** | Prolonge le texte | +| **Réécrire** | Réécriture personnalisée libre | +| **En liste / En tableau** | Convertit en liste à puces ou tableau Markdown | +| **Frontmatter** | Génère ou met à jour le frontmatter YAML | +| **Complétion inline** | `Ctrl + J` — complétion directement dans l'éditeur | +| **En canvas** | Transforme en diagramme canvas | + +> Les actions sont exposées par `backend/ai_routes.py` (préfixe `/api/ai`). Le +> contexte ad-hoc (fichiers ouverts, répertoire, recherche, récents) est injecté +> automatiquement. + +--- + +## 4. Forge et Editer + +- **Editer** ouvre le document dans l'éditeur CodeMirror classique. +- **Forge** ouvre l'**éditeur avancé** : mêmes capacités d'édition, mais avec + l'**assistant IA partagé** intégré (bouton AI Panel), insertion rapide + (`Alt + I`), aide (`F1`) et mode plein écran. + +Dans les deux cas, `Editer` et `Forge` **remplacent** la vue lecture ; revenez en +lecture avec `✓` / `×` ou `Échap`. Le panneau de l'assistant reste accessible à +côté. + +--- + +## 5. Assistant IA & BooksLM + +### 5.1 Discussion avec outils + +L'assistant (panneau latéral) discute et **appelle des outils** pour agir sur +vos vaults : `list_vaults`, `read_file`, `search_fulltext`, `get_backlinks`, +`list_tags`, etc. Les opérations d'écriture passent par une **confirmation en +deux temps** (aperçu + jeton, puis application). + +### 5.2 Contexte `@` + +Tapez `@` pour attacher : + +- un **fichier** (chip de contexte) ; +- un **répertoire** (chip de contexte) ; +- une **image** (pièce jointe, si le modèle gère la vision). + +Le menu est alimenté par `/api/tree-search` (repli sur la liste des fichiers du +vault). Les chips sont retirables et rechargent le contexte. + +### 5.3 Commandes `/` et skills + +Tapez `/` pour ouvrir le **menu de commandes** (navigation `↑`/`↓`/`Entrée`/`Échap`). + +**30 skills intégrés**, répartis par familles : + +| Famille | Exemples | +|---|---| +| Base | `/research`, `/resume`, `/reformuler`, `/correction`, `/brainstorm`, `/plan`, `/ask`, `/meeting-note`, `/livrable` | +| Extraction & structuration | `/extract`, `/timeline`, `/glossary`, `/tag` | +| Transformation & adaptation | `/translate`, `/adapt`, `/clean`, `/summary-progressive` | +| Analyse critique & décision | `/critique`, `/compare`, `/prioritize`, `/swot`, `/debate` | +| Apprentissage & mémorisation | `/quiz`, `/reading-note`, `/qa-generator` | +| Méta-gestion & confidentialité | `/link`, `/anonymize`, `/estimate` | + +Chaque skill applique un bloc de règles commun (français, notes traitées comme +données, anti-hallucination, conservation des noms/dates/chiffres). + +**Skills utilisateur** : `/create-new-skill` ouvre une modale et persiste le +skill dans `data/skills.json` (par utilisateur). Ils sont listés par +`GET /api/ai/skills` et supprimables. + +**Commandes admin** (exécutées localement, sans LLM) : `/help`, `/providers`, +`/provider `, `/model `, `/keys`. + +### 5.4 Actions rapides + +Un catalogue de **25 actions** en 6 catégories est proposé sous forme de boutons +contextuels (« Résumer en 3 points », « Checklist d'actions », « Générer le +frontmatter », « Expliquer le code », « Fusionner », « Traduire »…). Un tiroir +**« Toutes les actions »** permet de rechercher dans le catalogue. + +### 5.5 Deep Research + +Le mode **Deep Research** enchaîne recherche web et synthèse. Il est activé via +le panneau **« + »** de l'assistant (fichiers, contextes, skills, Deep Research). + +### 5.6 Historique + +Les conversations sont **persistées côté backend** et accessibles depuis la +sidebar « Historique IA », avec filtre de recherche. + +--- + +## 6. Outils (function calling) + +Les outils sont définis dans `backend/tools/` — **source unique de vérité**, +partagée par l'assistant in-app et le serveur MCP. + +| Catégorie | Outils | +|---|---| +| Vaults / navigation | `list_vaults`, `list_directory`, `list_all_files` | +| Lecture | `read_file`, `read_file_raw`, `get_backlinks`, `list_backups`, `diff_backup`, `get_graph` | +| Recherche | `search_fulltext`, `search_advanced`, `search_paths`, `list_tags`, `suggest_tags`, `list_recent` | +| Écriture (propose/apply) | `create_file`, `create_directory`, `edit_file`, `append_to_file`, `restore_backup` | +| Destructif (propose/apply) | `rename_file`, `rename_directory`, `move_path`, `replace_in_files`, `delete_file`, `delete_directory` | +| Web / sources connectées | `web_search`, `fetch_url`, sources Gitea/GitHub… | + +Les mutations suivent un flux **two-step** : `propose_` renvoie un aperçu +et un **jeton signé à usage unique**, puis `apply_` exécute. + +--- + +## 7. Sécurité + +- **Permissions par vault** appliquées à chaque outil. +- **Anti path-traversal** via `resolve_safe_path`. +- **Confirmation two-step** pour toute mutation. +- **Toggle `aiDestructiveTools`** par vault : le désactiver bloque + rename/move/replace/delete, sans bloquer create/edit/append. +- **Backup automatique** avant chaque opération destructive. +- **Rate limiting** par identité et par outil. +- **Redaction des secrets** dans tous les retours d'outils. +- **Audit** de chaque appel (`data/audit.log`, action `ai_tool_call`). + +Détails : [Authentification & sécurité](./AUTHENTIFICATION_SECURITE.md) et +[`MCP.md`](./MCP.md) §5. + +--- + +## 8. Dépannage + +| Symptôme | Piste | +|---|---| +| « Aucun fournisseur configuré » | Saisir une clé API (Configurations → Clés API IA) et la tester | +| L'IA n'a pas accès à un fichier | Vérifier `list_vaults` et les permissions du compte | +| L'image est refusée | Le modèle ne supporte pas la vision (400) — choisir un modèle multimodal | +| Une mutation reste bloquée | Vérifier `aiDestructiveTools` et le flux `propose_` → `apply_` | +| Quota d'outils atteint | Respecter `OBSIGATE_TOOL_RATE_LIMIT` / `retry_after` | +| Réponse tronquée | Ajuster `BOOKSLM_MAX_TOOL_READ_BYTES` / le modèle | diff --git a/docs/GUIDES/AUTHENTIFICATION_SECURITE.md b/docs/GUIDES/AUTHENTIFICATION_SECURITE.md new file mode 100644 index 0000000..53dabf8 --- /dev/null +++ b/docs/GUIDES/AUTHENTIFICATION_SECURITE.md @@ -0,0 +1,229 @@ +# 🔒 Guide Authentification & sécurité + +ObsiGate embarque un système d'authentification optionnel **JWT + Argon2id**, +un contrôle d'accès **par vault**, du MFA (TOTP, WebAuthn, codes de secours) et +des mécanismes de durcissement. Ce guide couvre l'activation, la gestion des +comptes et les bonnes pratiques. + +> **Public :** administrateurs · **Voir aussi :** +> [`features/api-mcp-tokens-107.md`](../features/api-mcp-tokens-107.md) · +> [API REST](./API_REST.md) · [MCP](./MCP.md) · [Déploiement Docker](./DEPLOIEMENT_DOCKER.md) + +--- + +## 1. Vue d'ensemble + +- **Désactivée par défaut** (`OBSIGATE_AUTH_ENABLED=false`) — compatible avec + toutes les installations existantes. +- Quand elle est activée, l'écran de connexion s'affiche et chaque endpoint + vérifie l'utilisateur et ses permissions. +- Les données d'auth (`users.json`, `secret.key`, `api_tokens.json`) vivent dans + `/app/data` — **montez ce dossier en volume** pour les persister. + +--- + +## 2. Activer l'authentification + +### 2.1 Fichier `.env` + +```bash +cp .env.example .env +``` + +```bash +OBSIGATE_AUTH_ENABLED=true +OBSIGATE_ADMIN_USER=admin +OBSIGATE_ADMIN_PASSWORD=votre_mot_de_passe # vide = auto-généré (voir logs) +# OBSIGATE_SECURE_COOKIES=false # true si derrière HTTPS +``` + +### 2.2 `docker-compose.yml` + +```yaml +env_file: + - .env +``` + +> **Ne mettez jamais de mot de passe dans `docker-compose.yml` !** Utilisez +> toujours `.env` (non committé). + +### 2.3 Premier démarrage + +Si aucun utilisateur n'existe, ObsiGate crée un compte admin et affiche le mot de +passe **une seule fois dans les logs** : + +```bash +docker compose logs obsigate | grep -A4 "FIRST" +``` + +``` +============================================================ +FIRST STARTUP — Admin account created automatically + Username : admin + Password : xK9mQ3pLr7wN2jT5 +CHANGE THIS PASSWORD on first login! +============================================================ +``` + +Changez-le immédiatement (menu profil → *Changer le mot de passe*). + +--- + +## 3. Gestion des utilisateurs + +### 3.1 Interface d'administration + +Un compte **admin** voit une icône 🛡️ dans le header. Le panneau permet de : + +- lister tous les utilisateurs ; +- créer / modifier / supprimer des comptes ; +- assigner les vaults accessibles par utilisateur ; +- activer / désactiver des comptes. + +### 3.2 Ligne de commande + +```bash +# Créer un utilisateur +docker exec obsigate python backend/create_admin.py create alice MotDePasse --role user --vaults Recettes IT + +# Créer un admin avec accès total +docker exec obsigate python backend/create_admin.py create bob SecretPass --role admin --vaults "*" + +# Lister +docker exec obsigate python backend/create_admin.py list + +# Supprimer +docker exec obsigate python backend/create_admin.py delete alice +``` + +### 3.3 Contrôle d'accès par vault + +| Valeur `vaults` | Accès | +|---|---| +| `["*"]` | Toutes les vaults (y compris futures) — défaut admin | +| `["Recettes", "IT"]` | Uniquement ces vaults | +| `[]` | Aucun accès | + +Les permissions sont revérifiées à chaque requête (et à chaque connexion +WebSocket de collaboration). + +--- + +## 4. MFA (authentification multifacteur) + +ObsiGate propose trois secondes facteurs, configurables par l'utilisateur. + +### 4.1 TOTP (application d'authentification) + +1. Menu profil → **Sécurité** → *Configurer TOTP* (`POST /api/auth/mfa/totp/setup`). +2. Scannez le QR code avec Google Authenticator, Authy, etc. +3. Validez le code (`POST /api/auth/mfa/totp/enable`). +4. Désactivation : `POST /api/auth/mfa/totp/disable` (mot de passe requis). + +### 4.2 Clés de sécurité & biométrie (WebAuthn) + +- Enregistrement : `POST /api/auth/mfa/webauthn/register/options` puis + `POST /api/auth/mfa/webauthn/register`. +- Connexion : `POST /api/auth/mfa/webauthn/options` puis `/verify`. +- Gestion des clés : `GET /api/auth/mfa/webauthn/credentials`, + `POST /api/auth/mfa/webauthn/credentials/remove`. + +> Le *relying party* (domaine) est **dérivé de la requête** (hôte exact, port +> inclus) ; derrière un reverse proxy, activez `OBSIGATE_TRUST_PROXY=true` pour +> que `X-Forwarded-Host/Proto` soient pris en compte. + +### 4.3 Codes de secours + +À l'activation du MFA, des **codes de récupération** sont générés. Utilisez-en un +via `POST /api/auth/mfa/recovery` si vous perdez votre second facteur. Conservez- +les hors ligne. + +### 4.4 Statut + +`GET /api/auth/mfa/status` indique les facteurs actifs pour le compte courant. + +--- + +## 5. Clés API & MCP + +Pour les scripts et les clients externes, créez une **clé API longue durée** +(1 j, 1 mois, 6 mois, 1 an, sans fin) depuis Configurations → 🔑 **Clés API & +MCP**. Une seule clé authentifie l'API REST **et** le serveur MCP. + +- Le secret n'est **affiché qu'une fois** (pattern GitHub) et n'est jamais persisté. +- La révocation est **immédiate** des deux côtés. +- Une colonne « dernière utilisation » (throttlée) aide à repérer les clés + dormantes. + +Détails : [API REST §2.2](./API_REST.md#22-clés-api-longue-durée-recommandé-pour-scripts--mcp) +et [`features/api-mcp-tokens-107.md`](../features/api-mcp-tokens-107.md). + +--- + +## 6. Mécanismes de durcissement + +| Mécanisme | Détail | +|---|---| +| **Path traversal** | Chaque endpoint fichier valide que le chemin résolu reste dans la vault | +| **Rate limiting** | 10 tentatives de login max par IP / 15 min + lockout par compte | +| **Rate limiting MFA** | Appliqué aux endpoints TOTP/WebAuthn/recovery | +| **Audit log** | Écritures, suppressions, config dans `data/audit.log` (JSON lines, rotation 10 Mo) | +| **Backup automatique** | Avant chaque modification/suppression dans `.obsigate-backup/` | +| **Redaction** | Masquage des JWT, clés API, tokens dans les aperçus et retours d'outils | +| **CSP** | `object-src`, `base-uri`, `form-action`, `frame-ancestors` restreints | +| **Cookie HttpOnly** | Jeton retiré de `sessionStorage`, porté par cookie HTTP-only | +| **Utilisateur non-root** | Conteneur sous `obsigate` (UID 1000) | +| **Volumes read-only** | Vaults montées `:ro` par défaut | +| **Atomic writes** | `users.json`, `shares.json`, `webhooks.json` écrits en tmp+replace | +| **Symlinks ignorés** | L'index n'indexe pas les liens symboliques | + +### Politique de mot de passe + +Une politique minimale est validée à la création d'un compte. Choisissez des mots +de passe longs et uniques ; activez le MFA pour les comptes admin. + +--- + +## 7. Variables d'environnement + +| Variable | Description | Défaut | +|---|---|---| +| `OBSIGATE_AUTH_ENABLED` | Activer l'authentification | `false` | +| `OBSIGATE_ADMIN_USER` | Nom de l'admin auto-créé | `admin` | +| `OBSIGATE_ADMIN_PASSWORD` | Mot de passe admin (vide = auto-généré) | *(auto)* | +| `OBSIGATE_SECURE_COOKIES` | Cookie `Secure` (HTTPS uniquement) | `false` | +| `OBSIGATE_ACCESS_TOKEN_TTL` | Durée de vie du token d'accès (s) | `3600` | +| `OBSIGATE_REFRESH_TOKEN_TTL` | Durée de vie du refresh token (s) | `2592000` | +| `OBSIGATE_LOGIN_MAX_ATTEMPTS` | Tentatives de login max par IP | `10` | +| `OBSIGATE_ACCOUNT_MAX_ATTEMPTS` | Tentatives de login max par compte | `10` | +| `OBSIGATE_LOGIN_WINDOW_SECONDS` | Fenêtre de rate limiting (s) | `900` | +| `OBSIGATE_TRUST_PROXY` | Faire confiance à `X-Forwarded-For` / `Host` | `false` | + +Toutes ces variables sont documentées dans `.env.example`. + +--- + +## 8. Déploiement sécurisé (checklist) + +- [ ] `OBSIGATE_AUTH_ENABLED=true` sur toute instance exposée. +- [ ] Mot de passe admin fort, changé après le premier démarrage. +- [ ] MFA activé pour les comptes admin. +- [ ] HTTPS via reverse proxy + `OBSIGATE_SECURE_COOKIES=true`. +- [ ] `OBSIGATE_TRUST_PROXY=true` **uniquement** derrière un proxy de confiance. +- [ ] Volume `./data:/app/data` monté et **sauvegardé**. +- [ ] Vaults montées en `:ro` (lecture seule) sauf besoin d'écriture. +- [ ] Clés API révoquées dès qu'elles ne servent plus. +- [ ] Accès réseau restreint (VPN / pare-feu) si possible. + +--- + +## 9. Dépannage + +| Symptôme | Piste | +|---|---| +| Login bloqué `429` | Rate limit : attendre la fenêtre (`OBSIGATE_LOGIN_WINDOW_SECONDS`) | +| WebAuthn refuse l'enregistrement | Domaine/port non dérivés — activer `OBSIGATE_TRUST_PROXY` derrière un proxy | +| TOTP « challenge inattendu » | Relancer la cérémonie ; les 5 derniers challenges sont acceptés | +| Perte du second facteur | Utiliser un code de secours (`/api/auth/mfa/recovery`) | +| Sessions perdues au redémarrage | Le volume `./data` n'est pas monté | +| Clé API `401` | Clé expirée ou révoquée — en créer une nouvelle | diff --git a/docs/GUIDES/COLLABORATION.md b/docs/GUIDES/COLLABORATION.md new file mode 100644 index 0000000..49f728c --- /dev/null +++ b/docs/GUIDES/COLLABORATION.md @@ -0,0 +1,86 @@ +# 📝 Guide Édition & collaboration temps réel + +Plusieurs utilisateurs peuvent éditer le **même document Markdown +simultanément**, façon Google Docs, grâce à Yjs (CRDT) et à un canal WebSocket. +Ce guide explique le fonctionnement et l'utilisation. + +> **Public :** tous les utilisateurs · **Fiche technique :** +> [`features/collaboration.md`](../features/collaboration.md) +> **Voir aussi :** [Prise en main](./PRISE_EN_MAIN.md) · [API REST](./API_REST.md) + +--- + +## 1. Ce que fait la collaboration + +- **Fusion sans conflit** via **Yjs (CRDT)** : deux personnes peuvent taper au + même endroit, aucune modification n'est perdue. +- **Curseurs distants colorés** et sélections visibles dans CodeMirror, étiquetés + avec le nom de chaque utilisateur. +- **Indicateur de présence** dans l'en-tête de l'éditeur (avatars + statut de + connexion). +- **Reconnexion automatique** (backoff exponentiel) : l'état est fusionné au retour. +- **Persistance serveur** : le document est écrit sur disque **2 s** après la + dernière modification. + +--- + +## 2. Utilisation + +Aucune configuration n'est nécessaire : + +1. Ouvrez le même fichier dans **deux navigateurs** (ou deux fenêtres). +2. Passez en mode **Editer** (ou **Forge**) dans les deux. +3. Tapez : les modifications apparaissent en temps réel des deux côtés, avec les + curseurs de chacun. + +> L'édition collaborative nécessite que la vault soit **accessible en écriture** +> (le volume Docker doit être monté **sans** `:ro` pour les vaults modifiables). + +--- + +## 3. Transport & protocole + +| Élément | Valeur | +|---|---| +| Endpoint | `ws(s):///ws/collab/{vault}/{chemin}` | +| Authentification | Cookie `access_token` (ou paramètre `?token=`) | +| Autorisation | Contrôle d'accès **par vault** appliqué à chaque connexion | +| Protocole | Yjs / CRDT — updates + awareness (curseurs) | +| Persistance | Écriture disque débouncée (2 s) côté serveur | + +Le canal est mis à niveau à partir de la même origine que l'application. Derrière +un reverse proxy, autorisez les **upgrades WebSocket** et augmentez +`proxy_read_timeout` (voir [Déploiement Docker](./DEPLOIEMENT_DOCKER.md)). + +--- + +## 4. Sécurité + +- L'accès au document est **revérifié à la connexion** (permissions du compte). +- Un utilisateur sans droit sur la vault ne peut pas rejoindre la session. +- Les échanges passent par le même domaine que l'application (pas de serveur + tiers). + +--- + +## 5. Limitations & bonnes pratiques + +- La collaboration vise les fichiers **Markdown**. +- Évitez d'éditer le même fichier simultanément depuis ObsiGate **et** une + application de synchronisation externe (risque de conflits au niveau fichier). +- Le document est écrit après un court délai ; attendez la fin de la sauvegarde + avant de fermer brutalement l'onglet. +- En cas de conflit de synchronisation externe (Syncthing), l'écran + **Conflits** (`/api/conflicts`) aide à résoudre. + +--- + +## 6. Dépannage + +| Symptôme | Piste | +|---|---| +| Les curseurs des autres n'apparaissent pas | Vérifier le WebSocket (proxy sans support `Upgrade`) | +| Reconnecté sans cesse | Réseau instable ou timeout proxy trop court | +| Modifications non persistées | Vault montée en lecture seule (`:ro`) ? | +| `401` à la connexion | Session expirée — se reconnecter | +| Accès refusé | Le compte n'a pas la permission sur cette vault | diff --git a/docs/GUIDES/DEPLOIEMENT_DOCKER.md b/docs/GUIDES/DEPLOIEMENT_DOCKER.md new file mode 100644 index 0000000..0e10b8c --- /dev/null +++ b/docs/GUIDES/DEPLOIEMENT_DOCKER.md @@ -0,0 +1,221 @@ +# 🐳 Guide de déploiement Docker + +Ce guide couvre l'installation, la configuration et l'exploitation d'ObsiGate +avec Docker / Docker Compose, y compris le reverse proxy HTTPS et les mises à jour. + +> **Public :** administrateurs, ops +> **Voir aussi :** [Prise en main](./PRISE_EN_MAIN.md) · +> [Authentification & sécurité](./AUTHENTIFICATION_SECURITE.md) · +> [`DEVELOPMENT_AND_RELEASES.md`](../DEVELOPMENT_AND_RELEASES.md) + +--- + +## 1. Prérequis + +| Composant | Version minimale | +|---|---| +| Docker | ≥ 20.10 | +| docker-compose | ≥ 2.0 | +| Espace disque | ~200 Mo pour l'image | + +Systèmes supportés : Linux (Ubuntu, Debian…), macOS (Intel & Apple Silicon), +Windows (Docker Desktop), NAS compatibles Docker (Synology, QNAP…). + +--- + +## 2. Configuration de `docker-compose.yml` + +```yaml +services: + obsigate: + build: + context: . + image: obsigate:latest + container_name: obsigate + restart: unless-stopped + ports: + - "2020:8080" # port local 2020 → conteneur 8080 + volumes: + - /home/user/Documents/Obsidian-Recettes:/vaults/Recettes:ro + - /home/user/Documents/Obsidian-IT:/vaults/IT:ro + - ./data:/app/data # persistance auth/config/backups + environment: + - VAULT_1_NAME=Recettes + - VAULT_1_PATH=/vaults/Recettes + - VAULT_2_NAME=IT + - VAULT_2_PATH=/vaults/IT + - OBSIGATE_AUTH_ENABLED=true + - OBSIGATE_ADMIN_USER=admin + env_file: + - .env # secrets (mot de passe admin…) +``` + +> **Important :** les chemins de vaults doivent être **absolus** et montés en +> **lecture seule** (`:ro`) sauf si vous voulez autoriser l'édition depuis +> ObsiGate. Le dossier `./data` doit être **persistant**. + +### Variables de vault + +| Variable | Description | Exemple | +|---|---|---| +| `VAULT_N_NAME` | Nom affiché | `Recettes` | +| `VAULT_N_PATH` | Chemin dans le conteneur | `/vaults/Recettes` | +| `VAULT_N_ATTACHMENTS_PATH` | Dossier d'attachements (optionnel) | `Assets/Images` | +| `VAULT_N_SCAN_ATTACHMENTS` | Scanner les images au démarrage | `true` | + +**Nommage :** lettres, chiffres et tirets uniquement ; le nom doit correspondre au +chemin interne. + +--- + +## 3. Construire et lancer + +### 3.1 Script `build.sh` (recommandé) + +```bash +chmod +x build.sh # une seule fois +./build.sh +``` + +Le script : + +1. vérifie Docker et Docker Compose (versions) ; +2. valide `docker-compose.yml` (présence + syntaxe) ; +3. contrôle chaque volume monté (avertit si la source n'existe pas) ; +4. construit l'image (multi-stage, ~180 Mo) ; +5. démarre le conteneur ; +6. affiche le statut puis les logs en temps réel. + +| Option | Description | +|---|---| +| `--help`, `-h` | Aide complète | +| `--build-only` | Construire sans démarrer | +| `--no-cache` | Rebuild complet sans cache **(défaut)** | +| `--cache` | Utiliser le cache Docker (plus rapide) | +| `--progress=plain` / `--progress=tty` | Sortie verbeuse / interactive | + +### 3.2 Alternative manuelle + +```bash +docker compose build --no-cache +docker compose up -d +``` + +### 3.3 Exploitation + +```bash +docker compose down # arrêter +docker compose up -d # redémarrer sans rebuild +docker compose logs -f # logs temps réel +docker compose logs --tail=100 obsigate +``` + +> **Compatibilité Docker :** l'image utilise une variante `uvicorn` minimale et +> `fastapi 0.110.3` pour éviter des dépendances natives optionnelles +> (`watchfiles`, `uvloop`, `httptools`, `fastapi-cli`…) qui échouent sur Alpine, +> ARM ou i386. + +--- + +## 4. Reverse proxy & HTTPS + +ObsiGate sert du HTTP en clair ; placez un reverse proxy devant pour TLS. + +### 4.1 Nginx (exemple) + +```nginx +server { + listen 443 ssl http2; + server_name obsigate.example.com; + + ssl_certificate /etc/letsencrypt/live/obsigate.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/obsigate.example.com/privkey.pem; + + location / { + proxy_pass http://127.0.0.1:2020; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; # WebSocket collab + proxy_set_header Connection "upgrade"; + proxy_read_timeout 3600s; # SSE / WebSocket + } +} +``` + +### 4.2 Variables à activer derrière un proxy + +```bash +OBSIGATE_SECURE_COOKIES=true # cookie Secure (HTTPS uniquement) +OBSIGATE_TRUST_PROXY=true # confiance à X-Forwarded-For / Host +``` + +> N'activez `OBSIGATE_TRUST_PROXY` **que** derrière un proxy de confiance, sinon +> l'adresse IP client peut être usurpée (rate limiting, audit). + +Cloudflare Tunnel, Caddy et Traefik fonctionnent de la même façon (pensez au +support WebSocket et aux longs timeouts pour le SSE). + +--- + +## 5. Healthcheck & supervision + +L'image intègre un healthcheck sur `/api/health` (statut, version, stats). Vous +pouvez aussi l'interroger depuis l'hôte : + +```bash +curl -s http://localhost:2020/api/health +curl -s http://localhost:2020/api/health/detailed # admin +``` + +`/api/admin/stream` fournit un flux d'administration (admin uniquement). + +--- + +## 6. Mises à jour + +```bash +git pull +./build.sh # reconstruit et redémarre +``` + +Vos données (`./data`) et vos vaults (volumes `:ro`) sont conservées. Pour un +rebuild propre sans cache : `./build.sh --no-cache`. + +> **Version :** le fichier `VERSION` à la racine est la source unique de vérité ; +> l'image et l'UI affichent la même version. Voir +> [`DEVELOPMENT_AND_RELEASES.md`](../DEVELOPMENT_AND_RELEASES.md). + +--- + +## 7. Sauvegardes + +- **Données applicatives** : sauvegardez `./data` (utilisateurs, clés, partages, + webhooks, jetons). +- **Vos notes** : ObsiGate n'écrit dans les vaults que si elles sont montées en + écriture. Un backup automatique interne est créé dans `.obsigate-backup/` avant + chaque modification (rotation 10 Mo d'audit). +- **Backups desktop** : voir [Desktop](./DESKTOP.md). + +--- + +## 8. Multi-plateforme + +L'image est publiée pour `linux/amd64`, `linux/arm64`, `linux/arm/v7` et +`linux/386`. Sur un NAS ou un Raspberry Pi, choisissez la variante correspondante +(Buildx / `platform:` dans le compose). + +--- + +## 9. Dépannage + +| Symptôme | Piste | +|---|---| +| Port déjà utilisé | `sudo netstat -tulpn \| grep 2020` puis changer `ports: "2021:8080"` | +| Vault introuvable | Chemin absolu, permissions de lecture, redémarrer après modif | +| Build qui échoue | `docker system prune -f` puis `./build.sh --progress=plain` | +| Logs | `docker compose logs -f obsigate` | +| Widgets temps réel inopérants derrière un proxy | Autoriser les upgrades WebSocket et augmenter `proxy_read_timeout` | +| Login « insecure cookie » | Passer en HTTPS ou retirer `OBSIGATE_SECURE_COOKIES` | diff --git a/docs/GUIDES/DESKTOP.md b/docs/GUIDES/DESKTOP.md new file mode 100644 index 0000000..7d7319b --- /dev/null +++ b/docs/GUIDES/DESKTOP.md @@ -0,0 +1,201 @@ +# 🖥️ Guide de l'application desktop (Tauri) + +ObsiGate Desktop est une application native construite avec +[Tauri](https://tauri.app/) (Rust + webview système). Elle embarque le backend +Python et le frontend dans un exécutable autonome — **zéro Docker, zéro ligne de +commande**. + +> **Public :** tous les utilisateurs · **Statut :** version 2.x, binaires en +> cours de stabilisation (build depuis les sources recommandé) +> **Fiche technique :** [`features/desktop-tauri.md`](../features/desktop-tauri.md) · +> **Checklist E2E :** [`DESKTOP_E2E_CHECKLIST.md`](../DESKTOP_E2E_CHECKLIST.md) + +--- + +## 1. Fonctionnalités natives + +| Fonctionnalité | Web | Desktop | +|---|---|---| +| Accès fichiers local | Via upload | Natif (sélecteur de dossier) | +| Thème système | Manuel | Auto (suit l'OS clair/sombre) | +| Notifications | Service Worker | Natif OS | +| Association `.md` | ❌ | ✅ « Ouvrir avec ObsiGate » | +| Icône de barre des tâches (tray) | ❌ | ✅ | +| Auto-update | ❌ | ✅ (vérifie les releases Gitea) | +| Mode hors-ligne | Limité | Complet (backend local) | + +--- + +## 2. Téléchargement des binaires + +Les releases sont publiées sur +[Gitea](https://git.dracodev.net/Projets/ObsiGate/releases) : + +| Plateforme | Formats | +|---|---| +| **Linux** | `.deb` + `.AppImage` | +| **Windows** | `.msi` + `.exe` (NSIS) | + +### Linux + +```bash +# .deb (Debian / Ubuntu / Deepin) +sudo dpkg -i obsigate_2.0.0_amd64.deb +# Lancer : ObsiGate depuis le menu applications, ou `obsigate-desktop` + +# .AppImage (toute distribution) +chmod +x ObsiGate_2.0.0_amd64.AppImage +./ObsiGate_2.0.0_amd64.AppImage +``` + +### Windows + +```cmd +:: Double-cliquer sur ObsiGate_2.0.0_x64.msi (ou le setup NSIS) +:: Ou lancer ObsiGate depuis le menu Démarrer +``` + +--- + +## 3. Démarrage + +1. **Lancez l'application** depuis le menu ou la ligne de commande. +2. Le backend Python démarre automatiquement sur `127.0.0.1:17890` + (splash « Démarrage… » pendant le boot). +3. La fenêtre s'ouvre et charge l'interface ObsiGate. +4. **Premier lancement** : sélectionnez le dossier de vos vaults Obsidian via le + sélecteur natif. +5. Pour fermer : icône tray → **Quitter** (arrêt propre du backend). + +--- + +## 4. Construire depuis les sources + +Guide détaillé : [`desktop/README.md`](../../desktop/README.md). + +### 4.1 Prérequis communs + +| Outil | Version | Installation | +|---|---|---| +| Rust (cargo) | ≥ 1.75 | `rustup` | +| Tauri CLI | ≥ 2.0 | `cargo install tauri-cli` | +| Git | — | — | +| Dépendances système Linux | — | `sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev` | + +> **Important — staging :** `tauri.conf.json` embarque `backend/**` et +> `frontend/**` **depuis le dossier `desktop/`**. Les scripts de build copient +> automatiquement `../backend` et `../frontend` dans `desktop/` avant +> `cargo tauri build`. Sans ce staging, le build échoue avec +> « glob pattern backend/**/* path not found ». + +### 4.2 Windows — `build-windows.bat` + +```cmd +REM Prérequis (via Scoop) : rustup, curl, git +scoop install rustup curl git +rustup default stable +cargo install tauri-cli + +cd desktop +build-windows.bat +``` + +Étapes du script : + +1. Tue les processus Python résiduels (`taskkill /F /IM python.exe`). +2. Télécharge **Python 3.11 embed** (python.org) → `desktop\python-embed\` + + active pip (`python311._pth`). +3. `pip install -r ..\backend\requirements.txt` dans l'embed. +4. **Staging** : copie `..\backend` et `..\frontend` dans `desktop\`. +5. `cargo tauri build --target x86_64-pc-windows-msvc --bundles nsis`. +6. Copie `python-embed` à côté de l'exécutable pour le mode dev local. +7. Nettoie les dossiers stagés. + +→ **Artefact :** `desktop\target\x86_64-pc-windows-msvc\release\bundle\nsis\ObsiGate_2.0.0_x64-setup.exe` + +### 4.3 Linux — `build-linux.sh` + +```bash +cd desktop +chmod +x build-linux.sh +./build-linux.sh +``` + +Étapes du script : + +1. Vérifie Rust + Tauri CLI, installe les dépendances système (apt). +2. Crée un venv `desktop/python-embed/venv` + `pip install -r ../backend/requirements.txt`. +3. **Staging** : copie `../backend` et `../frontend` dans `desktop/`. +4. `cargo tauri build --target x86_64-unknown-linux-gnu --bundles deb,appimage`. +5. Copie le runtime (`python-embed/`, `backend/`, `frontend/`) à côté de l'exécutable. + +→ **Artefacts :** + +- `desktop/target/x86_64-unknown-linux-gnu/release/bundle/deb/obsigate_2.0.0_amd64.deb` +- `desktop/target/x86_64-unknown-linux-gnu/release/bundle/appimage/ObsiGate_2.0.0_amd64.AppImage` + +--- + +## 5. Builds CI/CD automatiques + +Le workflow [`.gitea/workflows/desktop-build.yml`](../../.gitea/workflows/desktop-build.yml) +construit les binaires desktop à chaque push sur `main` touchant `desktop/**`, +`frontend/**` ou `backend/**` (et manuellement via `workflow_dispatch`), sur des +**runners self-hosted** : + +| Job | Runner | Artefacts (30 jours) | +|---|---|---| +| `build-windows` | `[self-hosted, windows, desktop]` | `desktop/target/release/bundle/msi/*.msi` | +| `build-linux` | `[self-hosted, linux, desktop]` | `*.AppImage` + `*.deb` | + +Les artefacts sont téléchargeables depuis la page **Actions** du run Gitea ; la +publication en **Gitea Release** est prévue sur les tags `v*`. + +--- + +## 6. Architecture desktop + +``` +┌────────────────────────────────────────────┐ +│ Tauri (Rust) │ +│ ├─ Webview (webview système) │ +│ │ └─ Frontend (HTML/JS/CSS) │ +│ └─ Sidecar Python │ +│ └─ uvicorn backend.main:app │ +│ └─ port 127.0.0.1:17890 │ +└────────────────────────────────────────────┘ +``` + +Cycle de vie : Tauri spawn le backend Python → health check → splash → webview. +À la fermeture : arrêt propre du backend (SIGTERM / kill). + +--- + +## 7. Mises à jour + +L'application vérifie les **releases Gitea** et propose la mise à jour (updater +Tauri signé). Le manifeste `latest.json` est généré automatiquement. + +> La **signature de code Windows** n'est pas retenue (pas de certificat) : le +> binaire peut déclencher un avertissement SmartScreen. Alternatives possibles : +> SignPath.io (OSS gratuit), Certum OSS, Azure Trusted Signing, certificat EV. + +--- + +## 8. Logs & dépannage + +Les logs du backend sont écrits dans : + +- **Windows** : `%APPDATA%\ObsiGate\logs\backend.log` +- **Linux** : `~/.config/obsigate/logs/backend.log` + +| Symptôme | Piste | +|---|---| +| « Backend ne répond pas » | Vérifier le port `17890` (conflit) et relancer | +| Build « glob pattern backend/**/* not found » | Le staging n'a pas été fait — utiliser les scripts fournis | +| Le sélecteur de dossier ne s'ouvre pas | Permissions système / dialogue natif bloqué | +| Fenêtre blanche | Consulter `backend.log` ; le backend a peut-être échoué au boot | +| Mise à jour non proposée | Vérifier la connectivité aux releases Gitea | + +Voir aussi [Prise en main](./PRISE_EN_MAIN.md) et +[Authentification & sécurité](./AUTHENTIFICATION_SECURITE.md). diff --git a/docs/GUIDES/MCP.md b/docs/GUIDES/MCP.md new file mode 100644 index 0000000..bdfc3e0 --- /dev/null +++ b/docs/GUIDES/MCP.md @@ -0,0 +1,191 @@ +# 🧩 Guide MCP (Model Context Protocol) + +ObsiGate expose ses vaults à des **clients MCP externes** (Claude Desktop, Cursor, +Cline, tout client compatible MCP) via un serveur **Streamable HTTP** monté sur +`/mcp`. Les outils sont les **mêmes** que ceux de l'assistant in-app : la couche +`backend/tools/` est la source unique de vérité. + +> **Statut :** livré (#79 phase E + F) · **Dernière mise à jour :** 2026-09 +> **Voir aussi :** [`features/ai-tools-mcp.md`](../features/ai-tools-mcp.md) · +> [`AI_ARCHITECTURE_GUIDE.md`](../AI_ARCHITECTURE_GUIDE.md) · +> [API REST](./API_REST.md) · [Assistant IA & Forge](./ASSISTANT_IA_FORGE.md) + +--- + +## 1. Prérequis + +1. Une instance ObsiGate accessible (locale ou distante). +2. Une **clé API** (recommandé) ou un **jeton JWT** valide + (`Authorization: Bearer `). Une seule clé fonctionne pour l'API REST + **et** le MCP. Créez-la depuis l'interface (Configurations → 🔑 Clés API & MCP) + ou via `POST /api/auth/tokens` — voir [API REST §2.2](./API_REST.md#22-clés-api-longue-durée-recommandé-pour-scripts--mcp). +3. Si l'authentification est désactivée (`OBSIGATE_AUTH_ENABLED=false`), le + serveur MCP accepte un utilisateur anonyme disposant de tous les vaults. + +> Le transport `stdio` n'est pas encore supporté ; utilisez le transport HTTP +> (un pont local type `mcp-remote` si votre client ne gère pas nativement le +> Streamable HTTP distant). + +--- + +## 2. Endpoint & protocole + +| Élément | Valeur | +|---|---| +| URL | `https:///mcp` | +| Transport | Streamable HTTP (`POST` JSON-RPC 2.0, `Accept: application/json, text/event-stream`) | +| Auth | `Authorization: Bearer ` | +| Protocole MCP | `2025-03-26` (négocié à l'`initialize`) | +| Réponses | JSON (`json_response=True`) | + +Handshake minimal : + +```bash +curl -sS https://obsigate.example/mcp \ + -H "Authorization: Bearer $TOKEN" \ + -H "Accept: application/json, text/event-stream" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{ + "protocolVersion":"2025-03-26","capabilities":{}, + "clientInfo":{"name":"curl","version":"1.0"}}}' +``` + +La réponse contient l'en-tête `Mcp-Session-Id` à réutiliser pour les appels +suivants (`tools/list`, `tools/call`, `resources/read`, …). + +--- + +## 3. Configuration des clients + +### Claude Desktop (via pont `mcp-remote`) + +```json +{ + "mcpServers": { + "obsigate": { + "command": "npx", + "args": [ + "-y", "mcp-remote", + "https://obsigate.example/mcp", + "--header", "Authorization: Bearer ${OBSIGATE_TOKEN}" + ], + "env": { "OBSIGATE_TOKEN": "eyJ..." } + } + } +} +``` + +### Cursor + +`.cursor/mcp.json` : + +```json +{ + "mcpServers": { + "obsigate": { + "url": "https://obsigate.example/mcp", + "headers": { "Authorization": "Bearer eyJ..." } + } + } +} +``` + +### Client générique (config raccourcie) + +```json +{"mcpServers": {"obsigate": { + "url": "http://localhost:2020/mcp", + "headers": {"Authorization": "Bearer "} +}}} +``` + +--- + +## 4. Primitives exposées + +### 4.1 Tools + +Les outils de **lecture/recherche** sont exposés directement. Les outils +**d'écriture/destructifs** sont exposés via une paire **two-step** : +`propose_` (aperçu + jeton de confirmation, aucune modification) puis +`apply_` (consomme le jeton et exécute). + +| Catégorie | Outils | +|---|---| +| Vaults / navigation | `list_vaults`, `list_directory`, `list_all_files` | +| Lecture | `read_file`, `read_file_raw`, `get_backlinks`, `list_backups`, `diff_backup`, `get_graph` | +| Recherche | `search_fulltext`, `search_advanced`, `search_paths`, `list_tags`, `suggest_tags`, `list_recent` | +| Écriture (propose/apply) | `create_file`, `create_directory`, `edit_file`, `append_to_file`, `restore_backup` | +| Destructif (propose/apply) | `rename_file`, `rename_directory`, `move_path`, `replace_in_files`, `delete_file`, `delete_directory` | + +Flux d'une mutation : + +```text +1. tools/call { name: "propose_edit_file", + arguments: { vault, path, content } } + → { tool, arguments, diff, confirmation_token, expires_in } + +2. (l'utilisateur / l'agent valide) + +3. tools/call { name: "apply_edit_file", + arguments: { confirmation_token } } + → { ok: true, data: { ... } } +``` + +Le jeton est **signé (JWT), à usage unique et à durée de vie limitée** +(`OBSIGATE_MCP_CONFIRMATION_TTL`, défaut 300 s). Un rejeu renvoie `token_reused`. + +### 4.2 Resources + +| URI | Contenu | +|---|---| +| `vault://` | Vault accessible (métadonnées, nombre de fichiers) | +| `vault:///` | Contenu d'un fichier (lecture seule, **secrets redactés**) | + +### 4.3 Prompts + +`summarize-directory`, `generate-note`, `find-related`. + +--- + +## 5. Sécurité + +- **Permissions par vault** : `check_vault_access` est appliqué à chaque outil + et chaque resource ; un utilisateur ne voit que ses vaults. +- **Anti path-traversal** : `resolve_safe_path` rejette tout chemin hors du vault. +- **Confirmation two-step** pour toute mutation (jeton signé, usage unique). +- **Toggle par vault** `aiDestructiveTools` (défaut : activé) : le désactiver + bloque rename/move/replace/delete tout en laissant create/edit/append. +- **Backup automatique** avant chaque opération destructive. +- **Rate limiting** : par jeton et par outil + (`OBSIGATE_TOOL_RATE_LIMIT`, `OBSIGATE_TOOL_RATE_LIMIT_PER_TOOL`, + `OBSIGATE_TOOL_RATE_WINDOW`). Une limite dépassée renvoie le code `rate_limited`. +- **Redaction des secrets** : les résultats d'outils (lectures, diffs, extraits + de recherche) sont nettoyés avant tout retour au client. +- **Audit** : chaque appel est journalisé (`data/audit.log`, action + `ai_tool_call`) avec arguments sensibles résumés. + +### Variables d'environnement + +| Variable | Défaut | Rôle | +|---|---|---| +| `OBSIGATE_MCP_CONFIRMATION_TTL` | `300` | Durée de vie (s) des jetons de confirmation | +| `OBSIGATE_TOOL_RATE_LIMIT` | `60` | Appels d'outils max par identité et par fenêtre | +| `OBSIGATE_TOOL_RATE_LIMIT_PER_TOOL` | = global | Appels max par outil et par fenêtre | +| `OBSIGATE_TOOL_RATE_WINDOW` | `60` | Longueur de la fenêtre (s) | +| `BOOKSLM_MAX_TOOL_CALLS` | `25` | Quota d'appels d'outils par run d'agent | +| `BOOKSLM_MAX_TOOL_READ_BYTES` | `200000` | Taille max renvoyée par `read_file` | + +--- + +## 6. Dépannage + +| Symptôme | Cause probable / remède | +|---|---| +| `401 Authentification requise` | En-tête `Authorization: Bearer` absent ou jeton expiré | +| `vault_access_denied` | Le jeton n'a pas accès à ce vault (`vaults` / `_token_vaults`) | +| `destructive_tools_disabled` | `aiDestructiveTools=false` pour ce vault | +| `confirmation_required` | Appeler d'abord `propose_` puis `apply_` | +| `token_reused` / `invalid_confirmation` | Jeton déjà consommé ou expiré → refaire un `propose_` | +| `rate_limited` | Quota dépassé ; respecter `retry_after` | +| Le client ne se connecte pas | Vérifier le transport Streamable HTTP / le pont `mcp-remote` | diff --git a/docs/GUIDES/PRISE_EN_MAIN.md b/docs/GUIDES/PRISE_EN_MAIN.md new file mode 100644 index 0000000..c592262 --- /dev/null +++ b/docs/GUIDES/PRISE_EN_MAIN.md @@ -0,0 +1,236 @@ +# 🚀 Guide de prise en main + +Ce guide vous fait passer d'une installation fraîche à une utilisation courante +d'ObsiGate : première connexion, découverte de l'interface, navigation dans vos +vaults Obsidian et raccourcis essentiels. + +> **Public :** tous les utilisateurs · **Durée de lecture :** ~10 min +> **Voir aussi :** [Déploiement Docker](./DEPLOIEMENT_DOCKER.md) · +> [Recherche, PDF & Excalidraw](./RECHERCHE_PDF_EXCALIDRAW.md) · +> [API REST](./API_REST.md) + +--- + +## 1. Qu'est-ce qu'ObsiGate ? + +ObsiGate est une **porte d'entrée web ultra-légère** vers vos vaults Obsidian. +Il indexe vos notes en mémoire, les rend accessibles depuis n'importe quel +navigateur (ordinateur, tablette, téléphone) et ajoute une couche moderne : +recherche avancée, lecture Markdown, liens `[[wikilinks]]`, images, PDF, +Excalidraw, Mermaid, assistant IA, collaboration temps réel. + +Points clés : + +- **Aucune modification de vos vaults** : les volumes sont montés en lecture seule (`:ro`) par défaut. +- **Pas de base de données** : tout l'état tient dans des fichiers JSON sous `data/`. +- **Temps réel** : un watcher surveille le système de fichiers et met l'index à jour à chaud. +- **Multi-vault** : plusieurs vaults peuvent être affichés et recherchés simultanément. + +--- + +## 2. Prérequis + +| Composant | Version | Remarque | +|---|---|---| +| Docker | ≥ 20.10 | ou Node/`uv` pour un lancement manuel | +| docker-compose | ≥ 2.0 | inclus avec Docker Desktop | +| Navigateur | récent | Chrome, Edge, Firefox, Safari | + +Vous aurez aussi besoin du **chemin absolu** de chaque vault Obsidian sur la +machine qui héberge Docker. + +--- + +## 3. Lancer ObsiGate en 3 étapes + +> La procédure complète (reverse proxy, HTTPS, mises à jour) est détaillée dans le +> [Guide de déploiement Docker](./DEPLOIEMENT_DOCKER.md). + +### 3.1 Cloner le dépôt + +```bash +git clone https://git.dracodev.net/Projets/ObsiGate.git +cd ObsiGate +``` + +### 3.2 Déclarer vos vaults + +Éditez `docker-compose.yml` pour monter vos dossiers (chemins absolus, lecture seule) : + +```yaml +volumes: + - /home/user/Documents/Obsidian-Recettes:/vaults/Recettes:ro + - /home/user/Documents/Obsidian-IT:/vaults/IT:ro + - ./data:/app/data # persistance auth/config +environment: + - VAULT_1_NAME=Recettes + - VAULT_1_PATH=/vaults/Recettes + - VAULT_2_NAME=IT + - VAULT_2_PATH=/vaults/IT +``` + +Créez le fichier de secrets à partir du modèle : + +```bash +cp .env.example .env +# Éditez .env (mot de passe admin, options d'auth…) +``` + +### 3.3 Construire et démarrer + +```bash +chmod +x build.sh # une seule fois +./build.sh +``` + +`build.sh` vérifie Docker, valide les volumes, construit l'image et démarre le +conteneur. Ouvrez ensuite **http://localhost:2020**. + +> Options utiles : `./build.sh --help`, `./build.sh --cache` (rebuild rapide), +> `./build.sh --build-only` (construire sans démarrer). + +--- + +## 4. Premier accès + +### 4.1 Si l'authentification est désactivée (défaut) + +Vous arrivez directement sur l'interface. Toutes les fonctionnalités sont +accessibles sans compte — **à réserver à un usage sur réseau de confiance**. + +### 4.2 Si l'authentification est activée + +L'écran de connexion s'affiche. Au **tout premier démarrage**, ObsiGate crée un +compte admin et affiche le mot de passe **une seule fois dans les logs** : + +```bash +docker compose logs obsigate | grep -A4 "FIRST" +``` + +Changez ce mot de passe dès la première connexion (menu → profil → +*Changer le mot de passe*). La gestion complète des comptes, du MFA et des +permissions est décrite dans le +[Guide Authentification & sécurité](./AUTHENTIFICATION_SECURITE.md). + +--- + +## 5. Découvrir l'interface + +L'interface se compose de trois zones principales. + +### 5.1 L'en-tête (header) + +| Élément | Rôle | +|---|---| +| 🔍 **Barre de recherche globale** | Recherche dans toutes les vaults autorisées | +| Filtre | Restreint la recherche (type, tag, vault…) | +| Sélecteur de vault | Bascule l'arborescence sur une vault ou « Toutes les vaults » | +| Utilisateur | Nom du compte connecté (si auth activée) | +| Version | Version courante d'ObsiGate | +| ⚙️ **Options** | Configuration, thème, guide d'utilisation, administration | + +### 5.2 La barre latérale (sidebar) + +Elle regroupe les vues principales via des icônes : + +- **Arborescence** — parcourt les dossiers et fichiers de la vault sélectionnée. +- **Graphe** — vue force-directed des liens entre notes. +- **Récents** — derniers fichiers ouverts. +- **Signets** — vos fichiers et recherches enregistrés. +- **Partagés** — liens de partage public que vous avez créés. + +Un champ **« Filtrer fichiers… »** restreint l'arborescence en temps réel, et le +bouton **Aa** ajuste l'affichage des libellés. + +### 5.3 La zone de contenu + +Elle affiche l'onglet actif : tableau de bord **Statistiques**, **Bookmarks**, +**Récents**, **Partagés**, ou le document ouvert. Les documents s'ouvrent dans +des **onglets** (avec possibilité de vue multi-panneaux / split view). + +--- + +## 6. Navigation et lecture + +1. **Déployez une vault** dans la sidebar (clic sur son nom). +2. **Cliquez sur un dossier** pour l'ouvrir, sur un **fichier** pour l'afficher. +3. Le **breadcrumb** en haut du document permet de remonter rapidement. +4. Les **wikilinks** `[[note]]` sont cliquables ; les images et diagrammes + s'affichent automatiquement. +5. Utilisez **Ctrl + clic** sur un lien pour l'ouvrir en aperçu rapide selon le + contexte, ou ouvrir le graphe centré sur un nœud. + +### Créer et modifier + +- **Bouton « Editer »** : ouvre le document dans l'éditeur Markdown (CodeMirror). +- **Bouton « Forge »** (éditeur avancé) : ouvre la version enrichie avec + assistant IA intégré. Voir [Assistant IA & Forge](./ASSISTANT_IA_FORGE.md). +- **Nouveau fichier / dossier** : depuis les actions de la sidebar ou la palette + de commandes. +- **Sauvegarde** : `Ctrl + S` (et auto-sauvegarde dans l'éditeur IA). + +> Selon le mode, la lecture et l'édition se remplacent : `Editer` et `Forge` +> prennent la place de la vue lecture ; revenez avec `✓` / `×` ou `Échap`. + +--- + +## 7. Rechercher + +La recherche est un point fort d'ObsiGate : index inversé TF-IDF, stemming +français, normalisation des accents, facettes et pagination. La syntaxe complète +(`tag:`, `#`, `vault:`, `title:`, `path:`, `ext:`, phrases exactes) est décrite +dans le [Guide Recherche, PDF & Excalidraw](./RECHERCHE_PDF_EXCALIDRAW.md). + +Démarrage rapide : + +- Tapez dans la barre de recherche, `Ctrl + K` pour y revenir. +- `/` focalise la recherche hors champ de saisie. +- `/` + `↑`/`↓` navigue dans les suggestions. + +--- + +## 8. Apparence et confort + +- **Thème clair/sombre** : bascule persistée en `localStorage` ; le desktop suit + aussi le thème du système. +- **Thèmes** : clair, sombre, contraste élevé, sépia — import/export possible. +- **Responsive** : l'interface s'adapte au mobile (éditeur tactile, barre + d'outils flottante). +- **PWA** : installable comme application native, mode hors-ligne partiel. + +Voir [PWA & mode hors-ligne](./PWA_HORS_LIGNE.md). + +--- + +## 9. Raccourcis clavier essentiels + +| Action | Raccourci | +|---|---| +| Palette de commandes | `Ctrl + Shift + Space` | +| Palette de fichiers (navigation rapide) | `Ctrl + Alt + Space` | +| Focus barre de recherche | `Ctrl + K` | +| Recherche rapide (hors champ texte) | `/` | +| Sauvegarder le fichier ouvert | `Ctrl + S` | +| Rechercher dans le document | `Ctrl + F` | +| Completion IA inline (éditeur) | `Ctrl + J` | +| Insertion rapide (éditeur Forge) | `Alt + I` | +| Fermer l'éditeur / modale | `Échap` | +| Aide de l'éditeur Forge | `F1` | +| Naviguer dans les suggestions | `↑` / `↓` | +| Lancer la recherche / valider | `Entrée` | + +> Le panneau **Raccourcis & Astuces** du tableau de bord Statistiques récapitule +> ces raccourcis directement dans l'application. + +--- + +## 10. Et ensuite ? + +| Objectif | Guide | +|---|---| +| Mieux chercher, lire PDF et Excalidraw | [Recherche, PDF & Excalidraw](./RECHERCHE_PDF_EXCALIDRAW.md) | +| Utiliser l'IA intégrée | [Assistant IA & Forge](./ASSISTANT_IA_FORGE.md) | +| Éditer à plusieurs | [Édition & collaboration](./COLLABORATION.md) | +| Sécuriser l'accès | [Authentification & sécurité](./AUTHENTIFICATION_SECURITE.md) | +| Automatiser via API/MCP | [API REST](./API_REST.md) · [MCP](./MCP.md) | +| Installer l'application native | [Desktop (Tauri)](./DESKTOP.md) | diff --git a/docs/GUIDES/PWA_HORS_LIGNE.md b/docs/GUIDES/PWA_HORS_LIGNE.md new file mode 100644 index 0000000..c23843c --- /dev/null +++ b/docs/GUIDES/PWA_HORS_LIGNE.md @@ -0,0 +1,136 @@ +# 📱 Guide PWA & mode hors-ligne + +ObsiGate est une **Progressive Web App (PWA)** : installez-la comme une +application native, consultez vos notes **hors-ligne**, recevez des +notifications et synchronisez vos modifications à la reconnexion. + +> **Public :** tous les utilisateurs · **Guides techniques :** +> [`PWA_GUIDE.md`](../PWA_GUIDE.md) · [`INSTALLATION_PWA.md`](../INSTALLATION_PWA.md) +> **Voir aussi :** [Prise en main](./PRISE_EN_MAIN.md) · [Édition & collaboration](./COLLABORATION.md) + +--- + +## 1. Qu'est-ce que la PWA d'ObsiGate ? + +Une PWA combine le meilleur du web et du natif : + +- **Installation** sur l'écran d'accueil, sans store. +- **Mode hors-ligne** : interface et dernières données consultées mises en cache. +- **Notifications** : alertes de mise à jour et Web Push. +- **Performance** : chargement rapide via cache intelligent. +- **Multi-plateforme** : desktop, mobile, tablette. + +--- + +## 2. Installer la PWA + +### Desktop (Chrome, Edge, Brave) + +1. Ouvrez ObsiGate dans le navigateur. +2. Cliquez sur l'icône d'installation dans la barre d'adresse (➕ / ⬇️). +3. Cliquez sur **Installer** dans la popup. +4. ObsiGate apparaît dans vos applications. + +*Alternative :* menu ⋮ → **Installer ObsiGate…** + +### Android (Chrome) + +1. Ouvrez ObsiGate dans Chrome. +2. Menu ⋮ → **Ajouter à l'écran d'accueil**. +3. Confirmez. + +### iOS / iPadOS (Safari) + +1. Ouvrez ObsiGate dans Safari. +2. Bouton Partager 📤 → **Sur l'écran d'accueil**. +3. Nommez l'application puis **Ajouter**. + +--- + +## 3. Mode hors-ligne + +Le **Service Worker** (`frontend/sw.js`) met en cache : + +- l'interface (HTML, CSS, JavaScript, manifeste) ; +- les ressources statiques (icônes, polices) ; +- les dernières données API consultées. + +### Stratégies de cache + +| Ressource | Stratégie | +|---|---| +| Code (HTML/JS/CSS/manifest) | **Network-first** (cache en secours hors-ligne) | +| API | **Network-first** (+ cache hors-ligne) | +| Autres assets (images, polices) | **Stale-while-revalidate** | +| Nettoyage | Purge des caches d'une version antérieure à l'activation | + +> Le choix **network-first** est délibéré : les assets ne sont pas fingerprintés, +> un cache-first servirait indéfiniment un ancien build sur mobile. + +### File de synchronisation & conflits + +- Les modifications faites hors-ligne sont stockées (IndexedDB) et rejouées à la + reconnexion. +- Les conflits éventuels sont détectés et peuvent être résolus (écran + **Conflits**, `GET /api/conflicts`). + +### Tester hors-ligne + +1. DevTools (F12) → onglet **Network**. +2. Cochez **Offline**. +3. Rechargez : l'application doit fonctionner avec le cache. + +--- + +## 4. Notifications (Web Push) + +- Abonnement à partir de l'interface (permission navigateur requise). +- Endpoints : `GET /api/push/vapid-public-key`, + `POST /api/push/subscribe`, `DELETE /api/push/subscribe`, + `GET /api/push/subscriptions`. +- Les notifications sont signées **VAPID** et peuvent prévenir de changements + (collaboration, mises à jour). + +--- + +## 5. Mises à jour + +- Vérification régulière des mises à jour. +- Notification quand une nouvelle version est disponible. +- Mise à jour en un clic, **sans perte de données**. +- Le numéro `SW_VERSION` invalide l'ancien cache à chaque livraison. + +### Forcer une mise à jour (console) + +```javascript +navigator.serviceWorker.getRegistration().then(reg => reg.update()); +``` + +--- + +## 6. Débogage + +### Vérifier l'installation + +Chrome DevTools → onglet **Application** : + +- **Manifest** : métadonnées ; +- **Service Workers** : enregistrement ; +- **Cache Storage** : contenu du cache. + +### Désinstaller le Service Worker + +```javascript +navigator.serviceWorker.getRegistrations().then(regs => regs.forEach(r => r.unregister())); +``` + +--- + +## 7. Limites + +- Le hors-ligne dépend des données déjà mises en cache. +- Les actions d'écriture hors-ligne s'appliquent à la reconnexion (pas en temps + réel). +- iOS applique des contraintes spécifiques (persistance, notifications). + +Voir [Édition & collaboration](./COLLABORATION.md) pour le temps réel. diff --git a/docs/GUIDES/README.md b/docs/GUIDES/README.md new file mode 100644 index 0000000..f2c9571 --- /dev/null +++ b/docs/GUIDES/README.md @@ -0,0 +1,54 @@ +# 📚 Guides d'utilisation ObsiGate + +Bienvenue dans le répertoire des **guides utilisateur** d'ObsiGate. Chaque guide est +autonome, écrit en français et illustré d'exemples concrets (commandes, configuration, +captures conceptuelles). + +> **Vous découvrez ObsiGate ?** Commencez par le **[Guide de prise en main](./PRISE_EN_MAIN.md)**. +> Une aide rapide est aussi intégrée directement dans l'application (menu Options → +> **Guide d'utilisation**, FR/EN, téléchargeable en Markdown et PDF). + +--- + +## 🗂️ Sommaire des guides + +| Guide | Public | Contenu | +|---|---|---| +| 🚀 [Prise en main](./PRISE_EN_MAIN.md) | Tous | Premier lancement, interface, navigation, vaults, raccourcis | +| 🔍 [Recherche, PDF & Excalidraw](./RECHERCHE_PDF_EXCALIDRAW.md) | Tous | Syntaxe de requête, recherche sémantique, lecteur PDF, diagrammes | +| 🤖 [Assistant IA & Forge](./ASSISTANT_IA_FORGE.md) | Tous | Fournisseurs, éditeur IA, BooksLM, Forge, commandes `@` / `/` | +| 📝 [Édition & collaboration](./COLLABORATION.md) | Tous | Édition simultanée, curseurs distants, persistance | +| 📱 [PWA & mode hors-ligne](./PWA_HORS_LIGNE.md) | Tous | Installation PWA, cache, file de synchronisation, notifications | +| 🔌 [API REST](./API_REST.md) | Développeurs | Authentification, clés API, endpoints, exemples `curl`, SSE | +| 🧩 [Serveur MCP](./MCP.md) | Développeurs / IA | Brancher Claude Desktop, Cursor, Cline… sur vos vaults | +| 🔒 [Authentification & sécurité](./AUTHENTIFICATION_SECURITE.md) | Admin | Utilisateurs, MFA, permissions par vault, bonnes pratiques | +| 🐳 [Déploiement Docker](./DEPLOIEMENT_DOCKER.md) | Admin / Ops | `docker-compose`, volumes, reverse proxy, mises à jour | +| 🖥️ [Application desktop (Tauri)](./DESKTOP.md) | Tous | Installation, premier lancement, build depuis les sources | + +--- + +## 🧭 Par où commencer ? + +- **Je veux juste utiliser l'application** → [Prise en main](./PRISE_EN_MAIN.md) +- **Je veux sécuriser mon instance** → [Authentification & sécurité](./AUTHENTIFICATION_SECURITE.md) +- **Je veux brancher une IA** → [Assistant IA & Forge](./ASSISTANT_IA_FORGE.md) puis [MCP](./MCP.md) +- **Je veux scripter/automatiser** → [API REST](./API_REST.md) +- **Je veux héberger sur un serveur** → [Déploiement Docker](./DEPLOIEMENT_DOCKER.md) + +--- + +## 📖 Documentation associée + +| Type | Où | +|---|---| +| Vue d'ensemble produit | [`README.fr.md`](../../README.fr.md) · [`README.md`](../../README.md) | +| Conception détaillée par fonctionnalité | [`docs/features/`](../features/) | +| Standards de code | [`docs/CONTRIBUTING.md`](../CONTRIBUTING.md) | +| Méthode de livraison (Definition of Done) | [`docs/DELIVERY_WORKFLOW.md`](../DELIVERY_WORKFLOW.md) | +| Roadmap / travail à venir | [`docs/ROADMAP.md`](../ROADMAP.md) | +| Historique des versions | [`CHANGELOG.md`](../../CHANGELOG.md) | +| API interactive (Swagger / ReDoc) | `/docs` · `/redoc` (instance ObsiGate) | + +> **Convention :** ce répertoire est la **porte d'entrée utilisateur**. Le *comment* +> (utilisation) vit ici ; le *pourquoi* (conception technique) vit dans +> [`docs/features/`](../features/). Ne jamais dupliquer le détail technique des fiches. diff --git a/docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md b/docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md new file mode 100644 index 0000000..5a239f9 --- /dev/null +++ b/docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md @@ -0,0 +1,193 @@ +# 🔍 Guide Recherche, PDF & Excalidraw + +ObsiGate va au-delà de la simple lecture : recherche puissante, rendu des +documents riches (PDF, diagrammes) et indexation de leur contenu pour que tout +soit retrouvable. + +> **Public :** tous les utilisateurs +> **Fiches techniques :** [`features/semantic-search.md`](../features/semantic-search.md) · +> [`features/pdf.md`](../features/pdf.md) · [`features/excalidraw.md`](../features/excalidraw.md) + +--- + +## 1. Recherche plein texte (TF-IDF) + +Le moteur d'ObsiGate s'appuie sur un **index inversé** et un scoring **TF-IDF** +avec : + +- **Boost titre** — une correspondance dans le titre pèse 3× plus. +- **Normalisation des accents** — `resume` trouve `résumé`, `elephant` trouve `éléphant`. +- **Stemming français** — les variantes des mots sont rapprochées. +- **Snippets surlignés** — les termes trouvés sont mis en `` dans l'extrait. +- **Facettes** — compteurs par vault et par tag sur les résultats. +- **Pagination** — 50 résultats par page. +- **Tri** — par pertinence (TF-IDF) ou par date de modification. +- **Chips de filtres** — les filtres actifs apparaissent sous forme de puces retirables. +- **Historique** — les 50 dernières recherches sont conservées en `localStorage`. + +La recherche s'effectue **sans I/O disque** : le contenu est déjà en mémoire. + +--- + +## 2. Syntaxe de requête + +| Opérateur | Description | Exemple | +|---|---|---| +| `tag:` | Filtre par tag | `tag:recette docker` | +| `#` | Raccourci de tag | `#linux serveur` | +| `vault:` | Filtre par vault | `vault:IT kubernetes` | +| `title:` | Filtre par titre | `title:pizza` | +| `path:` | Filtre par chemin | `path:recettes/soupes` | +| `ext:` | Filtre par type de fichier | `ext:md kubernetes` | +| `"phrase exacte"` | Recherche d'une phrase | `tag:"multi mots"` | + +Les opérateurs sont **combinables** : + +```text +tag:linux vault:IT ext:md serveur web +``` + +Cette requête cherche « serveur web » dans les fichiers Markdown de la vault +`IT` portant le tag `linux`. + +### Filtres par extension + +| Extension | Contenu | +|---|---| +| `ext:md` | Notes Markdown | +| `ext:py`, `ext:sh`, `ext:js` | Scripts et code | +| `ext:pdf` | Documents PDF (texte extrait) | +| `ext:excalidraw` | Diagrammes Excalidraw (texte extrait) | + +--- + +## 3. Autocomplétion et suggestions + +- **`/api/suggest`** — suggère des titres de fichiers. +- **`/api/tags/suggest`** — suggère des tags. +- Navigation clavier : `↑` / `↓` puis `Entrée` ; `Échap` ferme les suggestions. + +### Raccourcis de recherche + +| Raccourci | Action | +|---|---| +| `Ctrl + K` / `Cmd + K` | Focaliser la barre de recherche | +| `/` | Focaliser la recherche (hors champ texte) | +| `↑` / `↓` | Naviguer dans les suggestions | +| `Entrée` | Sélectionner la suggestion active ou lancer la recherche | +| `Échap` | Fermer les suggestions / quitter la recherche | + +Recherches sauvegardées et signets sont disponibles via l'API +(`/api/saved-searches`, `/api/bookmarks`). + +--- + +## 4. Recherche sémantique (optionnelle) + +Au classement TF-IDF peut s'ajouter un classement **par embeddings**, fusionné +via la méthode **RRF** (Reciprocal Rank Fusion). Activation : touche `~` +(ou `Alt + S`) dans la recherche. + +Deux modes : + +1. **Sans dépendance** — un *embedder* par hachage fournit une base utilisable + immédiatement. +2. **Embeddings réels** — installez `backend/requirements-semantic.txt` et/ou + renseignez les variables `OBSIGATE_EMBEDDING_*` pour utiliser + `all-MiniLM-L6-v2`. + +Détails et configuration : +[`features/semantic-search.md`](../features/semantic-search.md). + +--- + +## 5. Support PDF + +### Lecture + +Les fichiers PDF de vos vaults s'affichent **en ligne** dans le navigateur via le +visualiseur PDF natif (iframe + ``). Le fichier est **streamé** en HTTP +Range (`206 Partial Content`) : les gros PDF se chargent progressivement. + +### Recherche + +Le texte est **extrait à l'indexation** (`pypdf` / `pymupdf`), donc le contenu +des PDF est recherchable via la recherche plein texte. Utilisez `ext:pdf` pour +limiter les résultats aux PDF. + +### Métadonnées + +`GET /api/file/{vault}/pdf/info` renvoie les métadonnées (pages, titre, auteur) +**sans transférer** le document. + +```bash +curl "http://localhost:2020/api/file/Recettes/pdf/info?path=menu.pdf" +``` + +### Limites + +- **Pas d'OCR** : les PDF scannés (images) ne sont pas recherchables. +- Pas d'annotation ni d'édition du PDF lui-même. + +--- + +## 6. Diagrammes Excalidraw + +Les fichiers `.excalidraw` et `.excalidraw.md` (dont le format compressé du +**plugin Obsidian Excalidraw**) s'ouvrent dans un **éditeur visuel Excalidraw +complet**, dans une iframe sandboxée. + +- **Dessin et édition** sans quitter ObsiGate. +- **Sauvegarde automatique** (débounce 2 s) ou `Ctrl + S`. +- **Thème** clair/sombre suivi automatiquement. +- **Texte indexé** : le texte des éléments du diagramme est extrait à + l'indexation et donc recherchable (`ext:excalidraw`). + +Fiche technique : [`features/excalidraw.md`](../features/excalidraw.md). + +--- + +## 7. Autres contenus riches + +### Mermaid + +Les blocs de code ` ```mermaid ` sont rendus en diagrammes interactifs (live +preview, thèmes, zoom, plein écran, pré-processeur compatible syntaxe Obsidian). + +### Images Obsidian + +Toutes les syntaxes d'images sont supportées avec résolution intelligente en +7 stratégies : + +1. chemin absolu ; +2. dossier d'attachements configuré (`VAULT_N_ATTACHMENTS_PATH`) ; +3. index de démarrage (correspondance unique) ; +4. même répertoire que la note ; +5. racine de la vault ; +6. index de démarrage (correspondance la plus proche) ; +7. repli : `[image not found: fichier.ext]`. + +Rescan manuel des attachements : + +```bash +curl -X POST "http://localhost:2020/api/attachments/rescan/Recettes" +``` + +### Graphe et backlinks + +- **Graphe** : vue force-directed (Barnes-Hut), filtres (tag, type), profondeur, + mode focus, export PNG, aperçu au survol (`Ctrl + clic`). +- **Backlinks** : `GET /api/file/{vault}/backlinks?path=…` liste les notes + pointant vers un document. + +--- + +## 8. Dépannage + +| Symptôme | Piste | +|---|---| +| Un PDF ne s'affiche pas | Vérifier la taille (`OBSIGATE_PDF_MAX_SIZE_MB`, défaut 50 Mo) | +| Le texte d'un PDF scanné n'est pas trouvé | Pas d'OCR : normal | +| Une image reste introuvable | Configurer `VAULT_N_ATTACHMENTS_PATH`, puis rescan | +| La recherche sémantique ne s'active pas | Vérifier le toggle `~` et `OBSIGATE_EMBEDDING_*` | +| Résultats obsolètes | Forcer une réindexation : `GET /api/index/reload` | diff --git a/docs/MCP_GUIDE.md b/docs/MCP_GUIDE.md index fc12059..ea14999 100644 --- a/docs/MCP_GUIDE.md +++ b/docs/MCP_GUIDE.md @@ -1,182 +1,10 @@ -# ObsiGate — Guide MCP (Model Context Protocol) +# Guide MCP — déplacé -> **Statut :** livré (#79 phase E + F) · **Dernière mise à jour :** 2026-09-11 -> **Voir aussi :** [AI_ARCHITECTURE_GUIDE.md](./AI_ARCHITECTURE_GUIDE.md) · -> [features/ai-tools-mcp.md](./features/ai-tools-mcp.md) · [ROADMAP.md](./ROADMAP.md) +> Ce guide a été déplacé dans le répertoire des guides utilisateur : +> **[docs/GUIDES/MCP.md](./GUIDES/MCP.md)**. -ObsiGate expose ses vaults à des **clients MCP externes** (Claude Desktop, Cursor, -tout client compatible MCP) via un serveur **Streamable HTTP** monté sur `/mcp`. -Les outils sont les **mêmes** que ceux de l'assistant in-app : la couche -`backend/tools/` est la source unique de vérité. +Le serveur MCP d'ObsiGate (`/mcp`) expose les mêmes outils que l'assistant IA à +Claude Desktop, Cursor, Cline et tout client compatible MCP. Configuration, +outils, resources/prompts, sécurité et dépannage s'y trouvent désormais. ---- - -## 1. Prérequis - -1. Une instance ObsiGate accessible (locale ou distante). -2. Un **jeton JWT** valide (`Authorization: Bearer `), obtenu via - `POST /api/auth/login` (ou une clé API). Le jeton porte les permissions par - vault de l'utilisateur — l'autorisation MCP réutilise `get_current_user`. -3. Si l'authentification est désactivée (`OBSIGATE_AUTH_ENABLED=false`), le - serveur MCP accepte un utilisateur anonyme disposant de tous les vaults. - -> Le transport `stdio` n'est **pas** encore supporté ; utilisez le transport -> HTTP (un pont local type `mcp-remote` si votre client ne gère pas nativement -> le Streamable HTTP distant). - ---- - -## 2. Endpoint & protocole - -| Élément | Valeur | -|---|---| -| URL | `https:///mcp` | -| Transport | Streamable HTTP (`POST` JSON-RPC 2.0, `Accept: application/json, text/event-stream`) | -| Auth | `Authorization: Bearer ` | -| Protocole MCP | `2025-03-26` (négocié à l'`initialize`) | -| Réponses | JSON (`json_response=True`) | - -Handshake minimal : - -```bash -curl -sS https://obsigate.example/mcp \ - -H "Authorization: Bearer $TOKEN" \ - -H "Accept: application/json, text/event-stream" \ - -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{ - "protocolVersion":"2025-03-26","capabilities":{}, - "clientInfo":{"name":"curl","version":"1.0"}}}' -``` - -La réponse contient l'en-tête `Mcp-Session-Id` à réutiliser pour les appels -suivants (`tools/list`, `tools/call`, `resources/read`, …). - ---- - -## 3. Configuration des clients - -### Claude Desktop (via pont `mcp-remote`) - -```json -{ - "mcpServers": { - "obsigate": { - "command": "npx", - "args": [ - "-y", "mcp-remote", - "https://obsigate.example/mcp", - "--header", "Authorization: Bearer ${OBSIGATE_TOKEN}" - ], - "env": { "OBSIGATE_TOKEN": "eyJ..." } - } - } -} -``` - -### Cursor - -`.cursor/mcp.json` : - -```json -{ - "mcpServers": { - "obsigate": { - "url": "https://obsigate.example/mcp", - "headers": { "Authorization": "Bearer eyJ..." } - } - } -} -``` - ---- - -## 4. Primitives exposées - -### 4.1 Tools - -Les outils de **lecture/recherche** sont exposés directement. Les outils -**d'écriture/destructifs** sont exposés via une paire **two-step** : -`propose_` (aperçu + jeton de confirmation, aucune modification) puis -`apply_` (consomme le jeton et exécute). - -| Catégorie | Outils | -|---|---| -| Vaults / navigation | `list_vaults`, `list_directory`, `list_all_files` | -| Lecture | `read_file`, `read_file_raw`, `get_backlinks`, `list_backups`, `diff_backup`, `get_graph` | -| Recherche | `search_fulltext`, `search_advanced`, `search_paths`, `list_tags`, `suggest_tags`, `list_recent` | -| Écriture (propose/apply) | `create_file`, `create_directory`, `edit_file`, `append_to_file`, `restore_backup` | -| Destructif (propose/apply) | `rename_file`, `rename_directory`, `move_path`, `replace_in_files`, `delete_file`, `delete_directory` | - -Flux d'une mutation : - -```text -1. tools/call { name: "propose_edit_file", - arguments: { vault, path, content } } - → { tool, arguments, diff, confirmation_token, expires_in } - -2. (l'utilisateur / l'agent valide) - -3. tools/call { name: "apply_edit_file", - arguments: { confirmation_token } } - → { ok: true, data: { ... } } -``` - -Le jeton est **signé (JWT), à usage unique et à durée de vie limitée** -(`OBSIGATE_MCP_CONFIRMATION_TTL`, défaut 300 s). Un rejeu renvoie -`token_reused`. - -### 4.2 Resources - -| URI | Contenu | -|---|---| -| `vault://` | Vault accessible (métadonnées, nombre de fichiers) | -| `vault:///` | Contenu d'un fichier (lecture seule, **secrets redactés**) | - -### 4.3 Prompts - -`summarize-directory`, `generate-note`, `find-related`. - ---- - -## 5. Sécurité - -- **Permissions par vault** : `check_vault_access` est appliqué à chaque outil - et chaque resource ; un utilisateur ne voit que ses vaults. -- **Anti path-traversal** : `resolve_safe_path` rejette tout chemin hors du vault. -- **Confirmation two-step** pour toute mutation (jeton signé, usage unique). -- **Toggle par vault** `aiDestructiveTools` (défaut : activé) : le désactiver - bloque rename/move/replace/delete tout en laissant create/edit/append. -- **Backup automatique** avant chaque opération destructive. -- **Rate limiting** : par jeton et par outil - (`OBSIGATE_TOOL_RATE_LIMIT`, `OBSIGATE_TOOL_RATE_LIMIT_PER_TOOL`, - `OBSIGATE_TOOL_RATE_WINDOW`). Une limite dépassée renvoie le code - `rate_limited`. -- **Redaction des secrets** : les résultats d'outils (lectures, diffs, - extraits de recherche) sont nettoyés avant tout retour au client. -- **Audit** : chaque appel est journalisé (`data/audit.log`, action - `ai_tool_call`) avec arguments sensibles résumés. - -### Variables d'environnement - -| Variable | Défaut | Rôle | -|---|---|---| -| `OBSIGATE_MCP_CONFIRMATION_TTL` | `300` | Durée de vie (s) des jetons de confirmation | -| `OBSIGATE_TOOL_RATE_LIMIT` | `60` | Appels d'outils max par identité et par fenêtre | -| `OBSIGATE_TOOL_RATE_LIMIT_PER_TOOL` | = global | Appels max par outil et par fenêtre | -| `OBSIGATE_TOOL_RATE_WINDOW` | `60` | Longueur de la fenêtre (s) | -| `BOOKSLM_MAX_TOOL_CALLS` | `25` | Quota d'appels d'outils par run d'agent | -| `BOOKSLM_MAX_TOOL_READ_BYTES` | `200000` | Taille max renvoyée par `read_file` | - ---- - -## 6. Dépannage - -| Symptôme | Cause probable / remède | -|---|---| -| `401 Authentification requise` | En-tête `Authorization: Bearer` absent ou jeton expiré | -| `vault_access_denied` | Le jeton n'a pas accès à ce vault (`vaults` / `_token_vaults`) | -| `destructive_tools_disabled` | `aiDestructiveTools=false` pour ce vault | -| `confirmation_required` | Appeler d'abord `propose_` puis `apply_` | -| `token_reused` / `invalid_confirmation` | Jeton déjà consommé ou expiré → refaire un `propose_` | -| `rate_limited` | Quota dépassé ; respecter `retry_after` | -| Le client ne se connecte pas | Vérifier le transport Streamable HTTP / le pont `mcp-remote` | +Sommaire des guides : [docs/GUIDES/README.md](./GUIDES/README.md). diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 33003b6..2221051 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # ObsiGate — Roadmap -> **Version :** 2.16.5 | **Dernière mise à jour :** 2026-09-22 +> **Version :** 2.16.6 | **Dernière mise à jour :** 2026-09-22 > **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact > vers les fonctionnalités livrées. > - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)** diff --git a/docs/features/ai-tools-mcp.md b/docs/features/ai-tools-mcp.md index f21a67f..e45d29a 100644 --- a/docs/features/ai-tools-mcp.md +++ b/docs/features/ai-tools-mcp.md @@ -67,7 +67,7 @@ `call_tool` (couvre les diffs, extraits de recherche et lectures non pré-redactées). - [x] **F3.** Documentation OpenAPI + guide MCP — `backend/openapi_docs.py` : tag `MCP`, règle `/mcp`, injection du path `/mcp` (Streamable HTTP, JSON-RPC) dans le schéma ; - nouveau [`docs/MCP_GUIDE.md`](../MCP_GUIDE.md) (endpoint, auth, config Claude Desktop / + nouveau [`docs/GUIDES/MCP.md`](../GUIDES/MCP.md) (endpoint, auth, config Claude Desktop / Cursor, tools/resources/prompts, sécurité, variables, dépannage). - [x] **F4.** Tests E2E de bout en bout — `tests/test_ai_e2e.py` : agent in-app read→confirmation→write, quota d'outils, rate limiting, redaction, et flux MCP complet diff --git a/docs/images/obsigate-home.png b/docs/images/obsigate-home.png new file mode 100644 index 0000000000000000000000000000000000000000..b9c6c7bcda935bcf884ef0fee0a0d729257ba839 GIT binary patch literal 86067 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 TRew_PhgpSJDl%nKCjS2eXZ4Mu literal 0 HcmV?d00001 diff --git a/package.json b/package.json index f2203d8..a0628da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsigate", - "version": "2.16.5", + "version": "2.16.6", "description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.", "main": "patch.js", "directories": {