7.6 KiB
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_nomabsent = 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/consommateursne renvoie queid,nom,jeton_prefixe,statut,cree_par,cree_le). - L'
iddu consommateur sert dejtiré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éecontexte: {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 enacces_requisen nommant l'accès manquant. Avec octroi et mission approuvée (enveloppe budgétaire), le service Spark appelle/distribueret 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
workspaceexiste pour les octrois, mais/distribuern'autorise aujourd'hui que les octrois de typeconsommateur. 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 octroiworkspace) reste à spécifier et à implémenter si Bruno l'exige — il changerait la sémantique d'autorisation de/distribueret 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 surapi_keys.jsonen clair (voir le document de sauvegarde/restauration, §5) ; leur branchement sur le coffre est un travail séparé de la présente Phase 2.