diff --git a/docs/COFFRE-contrat-distribution.md b/docs/COFFRE-contrat-distribution.md new file mode 100644 index 0000000..8a809f0 --- /dev/null +++ b/docs/COFFRE-contrat-distribution.md @@ -0,0 +1,156 @@ +# Coffre Secrets ObsiGate — Contrat de distribution (Phases 3 et 4) + +Référence du contrat pour les clients serveur-à-serveur : Spark +(issue #21), TikTak, Flowdeck, Kanbante (issue #22). Ce document ne +duplique aucune logique de chiffrement : le chiffrement reste +entièrement côté ObsiGate (enveloppe KEK → DEK → secret, AES-256-GCM). + +## 1. Endpoint + +``` +POST /api/coffres/distribuer +Authorization: Bearer +Content-Type: application/json +``` + +Corps (forme à plat, Phase 1b) : + +```json +{ + "reference": "coffre:/", + "contexte_type": "mission", + "contexte_id": "mission-123" +} +``` + +Forme imbriquée acceptée depuis la Phase 2 (client Spark) : + +```json +{ + "reference": "coffre:/", + "contexte": { "type": "mission", "id": "mission-123" } +} +``` + +La forme à plat reste supportée ; si les deux formes sont présentes, +la forme à plat est prioritaire. Le contexte est **obligatoire** : +sans `contexte_type`/`contexte_id` (ou sans `contexte.type`/`id`), +la requête est refusée avant toute authentification utile. + +Réponse en succès (200) : + +```json +{ "reference": "coffre:/", "valeur": "", "version": 1 } +``` + +La valeur n'existe qu'en mémoire chez l'appelant : elle ne doit +jamais être écrite dans un fichier, un journal, un prompt, une étape +d'activité ou un livrable. Le patron imposé à chaque app est +**distribuer → utiliser → oublier** ; à la fin de l'opération +(fin de mission, fin de synchronisation), l'environnement qui la +contenait est détruit et rien ne persiste. + +Prérequis côté ObsiGate : coffres activés +(`OBSIGATE_SECRETS_ENABLED`, sinon 503 `secrets_desactives`) et KEK +configurée (sinon 503 `kek_absente`). + +## 2. Authentification : consommateur nommé, jeton propre + +- Un consommateur est créé par un admin : + `POST /api/coffres/consommateurs` (`{"nom": "spark"}`), ou en une + seule opération avec ses octrois : + `POST /api/coffres/consommateurs/avec-octrois` + (`{"nom": "tiktak", "octrois": [{"coffre_slug": "...", + "secret_nom": "..."}]}` ; `secret_nom` absent = tout le coffre). +- Le jeton (`csk_…`) n'est rendu **qu'une seule fois**, à la création. + Il n'est jamais stocké en clair (seul son hash SHA-256 est persisté) + et jamais relisté (`GET /api/coffres/consommateurs` ne renvoie que + `id`, `nom`, `jeton_prefixe`, `statut`, `cree_par`, `cree_le`). +- L'`id` du consommateur sert de `jti` révocable : + `DELETE /api/coffres/consommateurs/{id}` révoque immédiatement + toutes ses distributions, sans toucher aux autres consommateurs. +- Ce jeton est **distinct** du jeton MCP de notes et de la session + SSO : une session utilisateur, même admin, ne distribue jamais + (401 `consommateur_invalide`). Il n'existe pas d'endpoint de + distribution par session (voir §6). +- Ajouter une app = enregistrer un consommateur + créer des octrois, + **sans modification du coffre** et sans toucher à ses secrets. + +## 3. Cas Spark (issue #21) + +- Consommateur nommé `spark` (convention, non imposée par le code : + l'autorisation porte sur le jeton, pas sur le nom). +- Contexte : `contexte_type = "mission"`, `contexte_id` = identifiant + de la mission. La forme imbriquée + `contexte: {type: "mission", id: }` est acceptée. +- Flux côté agent-commun (hors ObsiGate, rappelé ici pour la + correspondance des erreurs) : le LLM ne voit que la référence + `coffre:/` et un accès oui/non, jamais la valeur. Sans + octroi, la mission passe en `acces_requis` en nommant l'accès + manquant. Avec octroi et mission approuvée (enveloppe budgétaire), + le service Spark appelle `/distribuer` et injecte la valeur en + variable d'environnement de l'exécuteur, jamais dans le prompt. + Fin de mission = environnement détruit. +- Révocation dans ObsiGate (consommateur ou octroi) : la mission + suivante repasse en `acces_requis`. Limite assumée : une mission + déjà démarrée conserve son environnement jusqu'à sa fin. + +## 4. Erreurs (codes `detail`, stables depuis la Phase 1b) + +| HTTP | `detail` | Sens | Correspondance Spark | +| --- | --- | --- | --- | +| 400 | `contexte_manquant` | Contexte absent ou vide. | Erreur d'appel, pas un état d'accès. | +| 400 | `reference_invalide` | Référence mal formée ou nom invalide. | Erreur d'appel. | +| 401 | `consommateur_invalide` | Jeton inconnu (ni MCP, ni session). | Configurer le jeton consommateur. | +| 403 | `consommateur_revoque` | Consommateur révoqué. | `acces_requis` (ré-accorder). | +| 403 | `octroi_absent` | Aucun octroi (ou secret/coffre inexistant — anti-énumération, volontairement indistinct). | `acces_requis` (demander l'octroi). | +| 403 | `octroi_revoque` | Octroi révoqué. | `acces_requis` (ré-accorder). | +| 403 | `octroi_expire` | Octroi expiré (défaut 90 jours à la création). | `acces_requis` (renouveler). | +| 410 | `secret_expire` | Secret expiré, même avec un octroi actif. | Rotation du secret côté ObsiGate. | +| 503 | `secrets_desactives` / `kek_absente` / `crypto_indisponible` | Coffre désactivé, KEK absente, ou bibliothèque absente. | Panne ObsiGate, pas un refus d'accès. | + +Un octroi coffre-wide (`secret_id` nul) couvre tous les secrets du +coffre ; un octroi par secret ne couvre que ce secret. Un octroi +révoqué ne doit jamais être masqué par l'absence d'un autre octroi +(priorité : ok > révoqué > expiré > absent). + +## 5. Audit + +Chaque distribution est auditée (`distribution_ok` ou +`distribution_refusee`, avec référence, consommateur, contexte et +code de résultat). **Aucune entrée d'audit ne contient jamais une +valeur, un texte chiffré, un nonce ou un jeton.** La route journal +(`GET /api/coffres/{slug}/journal`) filtre en défense en profondeur +ces champs même s'ils apparaissaient un jour dans une entrée. +La copie manuelle à usage unique (admin, via consommateur temporaire +créé puis révoqué immédiatement, valeur copiée au presse-papiers +sans affichage) est auditée comme une distribution de contexte +`copie_manuelle`, avec la trace distincte de ce contexte. + +## 6. Limites et restes à faire (Phases 3–4, côté ObsiGate) + +- **Isolation workspace Flowdeck** : le type de cible `workspace` + existe pour les octrois, mais `/distribuer` n'autorise aujourd'hui + que les octrois de type `consommateur`. L'isolation « un workspace + ne peut pas utiliser l'octroi d'un autre » repose donc, pour + l'instant, sur le serveur Flowdeck lui-même (il ne demande que les + références de son workspace, avec son propre jeton consommateur). + Un contrôle serveur strict (contexte workspace vérifié côté + ObsiGate contre un octroi `workspace`) reste à spécifier et à + implémenter si Bruno l'exige — il changerait la sémantique + d'autorisation de `/distribuer` et doit être coordonné avec les + apps clientes avant tout changement. +- **Distribution par session** : non implémentée, volontairement. + Elle contredirait la règle Phase 1b (distribution réservée aux + consommateurs) et exigerait une ré-authentification qui n'existe + pas encore dans ObsiGate. La copie manuelle reste le patron + supporté (consommateur temporaire, admin seulement). +- **Clients par app** (TikTak, Flowdeck, Kanbante, agent-commun) : + à implémenter dans chaque dépôt, en branche + PR, jamais mergés + sans go. Patron commun : le serveur distribue, utilise, oublie ; + aucune valeur en cache ; si ObsiGate est indisponible, l'opération + échoue proprement, sans repli sur une ancienne valeur. +- **Lecteurs internes ObsiGate** (`ai.py`, `tools/secrets.py`) : + toujours branchés sur `api_keys.json` en clair (voir le document + de sauvegarde/restauration, §5) ; leur branchement sur le coffre + est un travail séparé de la présente Phase 2.