# 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).