[Coffre Secrets] Connecteurs par reference ObsiGate, isolation workspace (Phase 4) #26
@@ -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).
|
||||
Reference in New Issue
Block a user