diff --git a/docs/coffre-reference-flowdeck.md b/docs/coffre-reference-flowdeck.md new file mode 100644 index 0000000..4b739c1 --- /dev/null +++ b/docs/coffre-reference-flowdeck.md @@ -0,0 +1,117 @@ +# Flowdeck — Connecteurs à référence ObsiGate (Phase 4, issue ObsiGate #22) + +## Principe + +Un connecteur personnel d'un workspace peut pointer vers une référence +ObsiGate au lieu d'un jeton : + +``` +coffre:/ ex. coffre:flowdeck/gitea-lecture +``` + +* Le champ jeton du connecteur **devient** le champ référence : aucun + jeton n'est stocké pour ce connecteur (`secret_encrypted` reste vide). +* L'octroi est **toujours lié à un `workspace_id`** : créer un connecteur + à référence sans workspace est refusé (HTTP 400 / `ValueError`). +* À l'appel, le **serveur** Flowdeck appelle + `POST /api/coffres/distribuer` d'ObsiGate avec le jeton consommateur + Flowdeck et un contexte `flowdeck_workspace / `, utilise + la valeur **en mémoire**, puis l'oublie. La valeur n'est jamais + persistée, jamais mise en cache, jamais journalisée, jamais renvoyée + à une réponse API ni au LLM. + +## Configuration (serveur Flowdeck) + +| Variable | Défaut | Rôle | +| --- | --- | --- | +| `OBSIGATE_COFFRE_URL` | dérivée de `DRACO_SSO_URL` | Base ObsiGate pour `/api/coffres/distribuer` | +| `OBSIGATE_CONSUMER_TOKEN` | (vide = distribution désactivée) | Jeton **consommateur** Flowdeck enregistré dans ObsiGate | + +Le jeton consommateur est distinct du jeton MCP de notes et de la +session SSO. Il ne figure dans aucun log, rapport ou réponse. + +## Isolation workspace — ce qui est garanti, et sa limite honnête + +**Garanti côté Flowdeck, avant tout appel** (`connectors.resolve_secret`) : + +* un connecteur à référence ne se résout que pour son propre + `workspace_id` ; +* workspace A tentant d'utiliser le connecteur (donc la référence / + l'octroi) du workspace B reçoit un refus explicite + (`CoffreWorkspaceMismatch` : « … ne peut pas utiliser ce connecteur + (il appartient au workspace …) : octroi d'un autre workspace refusé ») + et **le distributeur n'est pas appelé** ; +* un `workspace_id` déjà posé sur un connecteur est immuable : on ne + transfère pas un octroi en éditant le connecteur ; +* un connecteur à référence testé/appelé sans `workspace_id` échoue + proprement, jamais avec la valeur d'un autre workspace. + +**Limite à connaître** : côté ObsiGate, l'octroi est accordé au +consommateur **Flowdeck** (un jeton par application), pas à un workspace +— le `contexte_id` transmis sert à la traçabilité, pas à un contrôle +d'accès côté ObsiGate. L'isolation par workspace repose donc sur le +contrôle Flowdeck ci-dessus. Une isolation appliquée côté ObsiGate +exigerait un consommateur par workspace : c'est un reste à faire +décidé côté ObsiGate (enregistrement de consommateurs par workspace), +pas un prérequis pour utiliser ce mécanisme. + +## Échecs propres, sans repli + +| Situation | Comportement Flowdeck | +| --- | --- | +| ObsiGate injoignable / timeout / HTTP 5xx | `CoffreIndisponible` — l'appel échoue, **aucune** valeur en cache n'est utilisée (il n'y en a pas) | +| Octroi révoqué / consommateur révoqué (403) | `CoffreAccesRefuse` — message « accès révoqué, à ré-accorder dans ObsiGate » | +| Octroi expiré (403) / secret expiré (410) | `CoffreAccesRefuse` avec le code ObsiGate, sans la valeur | +| Référence mal formée | `CoffreReferenceInvalide` (format `coffre:/`, slug 3–64, nom 3–128, minuscules, chiffres, `-`/`_`, aligné sur ObsiGate) | + +## Secrets déjà chiffrés : pas de big-bang + +Les connecteurs historiques (`secret_encrypted` Fernet) restent valides +tels quels. La migration est **opportuniste, connecteur par +connecteur** : + +1. (optionnel) rattacher d'abord le connecteur à son workspace + (`PATCH /api/agent/connectors/{id}` avec `workspace_id`) ; +2. puis basculer vers la référence (`PATCH …` avec + `coffre_reference: "coffre:/"`) — le jeton local dormant + n'est pas effacé automatiquement, il est simplement ignoré tant que + la référence est posée ; effacer la référence (`""`) le réactive ; +3. vérifier par un probe avec `workspace_id`, puis supprimer le jeton + local si besoin (rotation côté ObsiGate). + +## Chemin de migration par connecteur + +| Connecteur actuel | État dans cette branche | Chemin | +| --- | --- | --- | +| Personnels (`custom`, `discord`, `telegram`, `mcp`, `tiktak`, `obsigate`) | **Implémenté** : `workspace_id` + `coffre_reference`, résolution en mémoire, isolation testée | Basculer connecteur par connecteur (ci-dessus) | +| Natifs Gitea / GitHub | **Inchangés** : jetons globaux d'instance (`GITEA_TOKEN`, `GITHUB_TOKEN` en `.env`) | Créer un connecteur personnel workspace-scopé à référence vers le même service et l'utiliser à la place du natif, quand le besoin workspace par workspace se présente ; les natifs ne sont pas supprimés (pas de big-bang) | +| LLM global (`llm_config` id=1) et clés par utilisateur (`user_llm_keys`) | **Inchangés** : clés en clair en base (globale / par utilisateur, pas par workspace) | Travail ultérieur dédié : ajouter une colonne référence sur `user_llm_keys` + résolution par workspace, avec le même patron (distribution en mémoire, workspace obligatoire) ; hors périmètre de cette branche | +| OAuth Google / M365 (`connector_tokens`) | **Inchangés** : jetons OAuth chiffrés par (kind, utilisateur) | Hors périmètre : le flux OAuth ne se prête pas à une référence coffre sans repenser le refresh ; à traiter séparément si besoin | + +## API + +* `POST /api/agent/connectors` accepte `workspace_id` et + `coffre_reference` (référence sans workspace → 400). +* `PATCH /api/agent/connectors/{id}` accepte `workspace_id` + (premier rattachement seulement) et `coffre_reference` (`""` = + effacer la référence). +* `POST /api/agent/connectors/probe` accepte `workspace_id`. +* Réponses : `workspace_id`, `coffre_reference`, `secret_source` + (`coffre` | `chiffre_local` | `aucun`) — jamais de valeur. +* `GET /api/agent/connectors` liste les connecteurs avec ces champs. + +## Ce qui reste à faire (validation réelle) + +* Valider contre une vraie instance ObsiGate : créer le consommateur + Flowdeck + les octrois, puis un probe réel avec `workspace_id` + (les tests ici utilisent un faux endpoint de distribution). +* Threader `workspace_id` depuis l'agent : `AgentEngine.workspace_id` + → `connector_fetch` (outil LLM) et `McpTool.call_tool` — aujourd'hui + ces chemins appellent sans workspace et échouent proprement pour les + connecteurs à référence (refus, pas de fuite) ; les connecteurs + historiques ne sont pas affectés. +* Le cas échéant, consommateur ObsiGate par workspace (voir limite + ci-dessus) et bascule des natifs Gitea/GitHub et des clés LLM + (tableau ci-dessus). +* Interface : sélecteur de référence dans le formulaire connecteur + (aujourd'hui, champ `coffre_reference` via l'API seulement).