"""Plugins de l'instance — on/off à **effet réel** (v7.58.0). Chaque plugin câble une chose concrète, jamais un simple drapeau : - ``web-tools`` → retiré du registre d'outils de l'agent (schéma + exécution) - ``web-clipper`` → routers ``/extensions`` et ``/api/v2/web-clipper/*`` refusés - ``automations`` → routes ``/workspace/automations*`` refusées + scheduler Le garde de route est une **dépendance FastAPI posée à l'`include_router`** (``main.py``) : 3 lignes, aucun fichier de router touché. """ from __future__ import annotations from fastapi import HTTPException, Request from app.db import get_conn # slug → (nom affiché, description affichée) CATALOG: list[tuple[str, str, str]] = [ ("web-tools", "Outils web de l'agent", "Retire web_search et fetch_url du registre d'outils de l'agent"), ("web-clipper", "Web Clipper", "Coupe la page /extensions et l'API /api/v2/web-clipper/*"), ("automations", "Automations", "Coupe /workspace/automations* et le scheduler en arrière-plan"), ] # slug → outils LLM retirés du registre quand le plugin est OFF PLUGIN_TOOLS: dict[str, tuple[str, ...]] = {"web-tools": ("web_search", "fetch_url")} def is_enabled(slug: str) -> bool: """Ligne absente = activé (défaut sûr : ne rien couper par accident).""" with get_conn() as conn: row = conn.execute("SELECT enabled FROM plugins WHERE slug=?", (slug,)).fetchone() return True if row is None else bool(row["enabled"]) def list_plugins() -> list[dict]: with get_conn() as conn: rows = {r["slug"]: bool(r["enabled"]) for r in conn.execute("SELECT slug, enabled FROM plugins")} return [{"slug": slug, "name": name, "description": desc, "enabled": rows.get(slug, True)} for slug, name, desc in CATALOG] def set_enabled(slug: str, enabled: bool) -> dict: if not any(s == slug for s, _, _ in CATALOG): raise ValueError(f"Plugin inconnu: {slug}") name = next(n for s, n, _ in CATALOG if s == slug) desc = next(d for s, _, d in CATALOG if s == slug) with get_conn() as conn: # SQLite vérifie NOT NULL AVANT l'upsert : il faut fournir name/description. conn.execute( "INSERT INTO plugins (slug, name, description, enabled) VALUES (?,?,?,?) " "ON CONFLICT(slug) DO UPDATE SET enabled=excluded.enabled", (slug, name, desc, 1 if enabled else 0)) conn.commit() return next(p for p in list_plugins() if p["slug"] == slug) def plugin_required(slug: str): """Dépendance FastAPI : 404 dès que le plugin est désactivé. `Request` doit être importé au **module** : les annotations sont des chaînes (`from __future__ import annotations`) et FastAPI les résout dans les globales du module — sinon il lit un query param → 422. """ def dep(request: Request) -> None: # annotation Request OBLIGATOIRE : sans elle FastAPI lit un query param → 422 if not is_enabled(slug): raise HTTPException(status_code=404, detail=f"Plugin désactivé: {slug}") return dep