Files
ObsiGate/docs/COFFRE-contrat-distribution.md

7.6 KiB
Raw Permalink Blame History

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 <jeton-consommateur>
Content-Type: application/json

Corps (forme à plat, Phase 1b) :

{
  "reference": "coffre:<slug>/<nom>",
  "contexte_type": "mission",
  "contexte_id": "mission-123"
}

Forme imbriquée acceptée depuis la Phase 2 (client Spark) :

{
  "reference": "coffre:<slug>/<nom>",
  "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) :

{ "reference": "coffre:<slug>/<nom>", "valeur": "<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: <id-mission>} 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:<slug>/<nom> 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.