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