[Coffre Secrets] Connecteurs par reference ObsiGate, isolation workspace (Phase 4) #26

Merged
bruno merged 9 commits from feat/coffre-reference-obsigate into main 2026-10-28 00:15:18 -04:00
Showing only changes of commit 62b536c865 - Show all commits
+117
View File
@@ -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:<slug>/<nom> 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 / <workspace_id>`, 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>/<nom>`, 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:<slug>/<nom>"`) — 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).