Boucle d'approbation opérateur
Un agent qui communique avec des contreparties externes ne doit jamais envoyer de message de son propre chef et doit tenir l'opérateur informé en interne. Cette compétence définit le contrat : chaque brouillon sortant est déposé comme une obligation, un opérateur décide du texte exact, et un registre de livraison prouve ce qui est parti.
Quand l'utiliser
- Un agent rédige des réponses à des clients, fournisseurs, investisseurs ou partenaires dans un canal partagé, un e-mail ou un chat, et un humain doit approuver avant l'envoi.
- Vous avez besoin d'une piste d'audit liant chaque message envoyé au texte exact du brouillon, à l'opérateur qui l'a approuvé et au moment de la décision.
- Vous avez constaté qu'une approbation périmée libère un brouillon réécrit, ou que deux employés livrent le même message approuvé deux fois.
- Les brouillons redemandent à la contrepartie des informations que le registre contient déjà.
Fonctionnement
Objets
| Objet | Signification |
|---|---|
| Obligation | Une chose due à une contrepartie. Statuts : rédigé, approuvé, rejeté, envoyé. Comporte direction, contrepartie, canal et un horodatage updated_at. |
| Brouillon | Ligne satellite contenant le texte exact du brouillon, son empreinte sha256, les coordonnées d'origine (plateforme, canal, fil, utilisateur) et une priorité (P0 à P3). Un par obligation, remplacé en cas de nouvelle soumission. |
| Décision | Approbation ou rejet d'un opérateur, enregistré avec l'identifiant de l'opérateur, un nonce et l'horodatage du brouillon concerné. |
| Instantané d'approbation | Texte immuable, empreinte, horodatage et destination enregistrés par le rédacteur de décision autorisé. Les instantanés manquants ne peuvent pas autoriser l'envoi. |
| Revendication | Réservation durable avec un jeton aléatoire et un état ; au plus une revendication active par obligation. |
| Livraison | Ligne de registre prouvant un envoi ou un avis pour un couple (obligation, décision). |
Le schéma de référence se trouve dans references/approval-ledger.sql.
Dépôt d'un brouillon
- Nettoyer les entrées. Supprimer les caractères de contrôle, réduire les espaces blancs dans les champs sur une seule ligne, et appliquer des limites de longueur (brouillon, résumé, contexte, contrepartie). Les champs vides ou surdimensionnés sont refusés, pas tronqués silencieusement.
- Exécuter le garde-fou de base (ci-dessous). Il peut refuser le dépôt.
- Hacher le texte du brouillon avec sha256. Le préfixe de l'empreinte est ajouté au résumé pour que le panneau d'approbation montre quel texte il approuve.
- Mise à jour ou insertion. Si une obligation déjà rédigée existe pour la même (contrepartie, canal), remplacer le satellite du brouillon et avancer le
updated_atde l'obligation. Cet avancement est la rotation d'horodatage : toute décision indexée sur l'ancien horodatage ne peut plus libérer le nouveau texte. Sinon, insérer une nouvelle obligation avec le statutrédigé. - Acheminer l'accusé de réception du dépôt uniquement vers une destination interne opérationnelle configurée et vérifiée. Si l'origine est cette destination interne, accuser réception sur place. Jamais silencieux signifie un rapport interne, pas une réponse externe automatique. Garder les empreintes de brouillon, le statut d'approbation, l'identité de l'opérateur et les métadonnées du flux de travail hors des canaux visibles par la contrepartie. Les origines inconnues ou non classifiées restent silencieuses ; un message direct n'est pas automatiquement interne.
Si une destination interne vérifiée n'est pas disponible, conserver le résultat du dépôt dans le résultat de l'outil interne ou la surface opérateur. Ne jamais revenir à une origine externe ou inconnue. Un résultat d'outil exposé à des tiers n'est pas une surface interne.
Déposer un brouillon n'autorise pas une réponse externe. Toute question de clarification ou réponse neutre autorisée par la politique est une décision sortante distincte, soumise aux contraintes existantes de mention, de canal, de brouillon uniquement, de gel et de jamais dans la discipline de canal-contrepartie. Elle ne doit pas divulguer de métadonnées d'approbation internes.
Garde-fou de base
Avant de déposer un brouillon, interroger la base actuelle pour la contrepartie (un registre temporel, un stock de contrats ou un CRM) :
- Contrat signé ou livré enregistré : refuser le dépôt avec les preuves et une recommandation. Demander à une contrepartie des spécifications après signature est exactement l'échec que ce garde-fou vise à empêcher.
- Remplacement par l'opérateur :
force_despite_signed_contractlaisse passer le dépôt et ajoute[BASELINE_OVERRIDE_SIGNED_CONTRACT]dans le contexte du brouillon. - Service de garde-fou injoignable : le dépôt est effectué et le contexte est marqué
[BASELINE_CHECK_UNAVAILABLE]. Le panneau voit que la protection était désactivée. Les échecs ne désactivent jamais silencieusement le garde-fou. - Lorsque des faits sont disponibles, joindre les plus récents au contexte sous forme de résumé
[BASELINE FACTS: ...]pour que le brouillon atterrisse avec des informations actuelles.
Prise de décision
Le panneau d'approbation liste les obligations avec le statut rédigé et la direction nous_leur_devons. Approuver ou rejeter écrit une ligne de décision portant l'horodatage du brouillon (draft_updated_ts) et change le statut de l'obligation dans la même transaction. Une décision dont l'horodatage ne correspond pas au updated_at actuel est périmée et ne doit rien libérer.
Pour une décision d'approbation déjà autorisée, la même transaction insère une ligne immuable obligation_approval_snapshots : identifiants de la décision et de l'obligation, horodatage actuel du brouillon, texte exact et SHA-256, plateforme/canal/fil, et type draft_sent. Le rédacteur de décision doit établir l'autorisation avant d'écrire ; la référence n'authentifie jamais un opérateur ni ne fabrique de décision. La politique d'approbation automatique n'est ni activée ni étendue par la référence. Les décisions héritées sans instantané nécessitent une réconciliation explicite ou une nouvelle approbation ; ne jamais rétro céder d'autorisation à partir du brouillon mutable actuel.
Livraison
La référence SQLite est references/approval_claims.py. Elle accorde l'autorisation d'envoi mais n'appelle jamais le transport. Utiliser une base de données de référence locale existante initialisée à partir du fichier SQL ; le module n'applique pas de schéma ni de migrations de production. Seul un rédacteur de décision de confiance peut remplir les enregistrements d'approbation. Tous les rédacteurs doivent activer les clés étrangères et les déclencheurs récursifs et respecter les gardes du schéma ; la falsification administrative de la base de données est hors de ce modèle.
- Découvrir les brouillons approuvés liés. La découverte n'est pas une permission.
claim()ouvre sa propre transactionBEGIN IMMEDIATE, valide l'horodatage approuvé actuel, le texte exact, le SHA-256 calculé et la destination complète par rapport à l'instantané, et insère une revendication unique avant de retourner son jeton. Un conflit arrête le travailleur avant le transport. Les accusés de réception complétés ne peuvent pas être réclamés. begin_dispatch()revalide la liaison et change atomiquementrevendiquéenen cours d'envoien utilisant le jeton. Seul son appelant gagnant reçoit ledraft_textexact et la destination après validation. Ne jamais régénérer le texte, relire un satellite mutable pour le transport, ni réutiliser la charge utile pour une autre tentative. Une transaction d'appelant imbriquée est refusée ; l'autorisation ne peut pas dépendre d'une validation ultérieure de l'appelant. Aucune transaction de base de données ne reste ouverte pendant le transport.- Un résultat confirmé réussi va à
complete(), qui enregistre atomiquement la coordonnée de livraison, marque la revendication comme livrée et change le statut de l'obligation enenvoyé. Une réalisation identique est une opération nulle ; des coordonnées conflictuelles échouent. La clé UNIQUE de l'accusé de réception déduplique les enregistrements, pas les effets externes antérieurs. - Les exceptions, les dépassements de délai, la mort du travailleur après begin-dispatch, ou la persistance d'accusé de réception échouée laissent une tentative bloquée.
mark_unknown()enregistre l'incertitude. Les revendications inconnues n'expirent jamais, ne se rouvrent jamais, ne se réessayent jamais et ne permettent pas à une autre décision pour cette obligation de les contourner. Un appelant de confiance peut utiliserreconcile()avec une coordonnée et une preuve de succès confirmée ; le module ne vérifie pas cette preuve. Un accusé de réception absent n'est pas une preuve de non-livraison.
La garantie est une tentative d'envoi automatique par décision approuvée, pas une livraison externe exactement une fois. Un crash après begin-dispatch mais avant transport peut laisser zéro envoi et une revendication bloquée. Libérer un résultat inconnu pour une nouvelle tentative nécessiterait d'isoler l'exécuteur original et de vérifier la sémantique du fournisseur ; cette référence ne fournit délibérément aucune opération de réessai.
| État de la revendication | États suivants autorisés |
|---|---|
| revendiqué | en cours d'envoi ou annulé avant envoi |
| en cours d'envoi | livré ou inconnu |
| inconnu | livré uniquement via une réconciliation de confiance |
| livré, annulé | terminal ; la clé de décision ne peut pas être réutilisée |
Pendant qu'une revendication est active, les gardes de la base de données gèlent les écritures sur l'obligation, le brouillon et la décision, y compris les remplacements. Les instantanés et les revendications ne peuvent pas être effacés. Annuler une opération revendiquée avec son jeton avant de la redéposer ; le jeton périmé n'accorde alors rien. Une fois l'envoi commencé, garder les nouvelles modifications ou révocations pour la réconciliation. Cela sérialise les modifications au lieu de prétendre rappeler une opération en cours.
Les décisions rejetées et les lignes héritées sans satellites de brouillon/instantanés n'entrent jamais dans ce chemin d'envoi de brouillon externe. Les signaler sur la surface opérateur interne pour traitement manuel. Les pieds de page des accusés de réception internes restent internes :
approuvé par <opérateur> · accusé <decision_id> · empreinte sha256 <prefix>.
Ne jamais modifier le texte externe déjà approuvé pour y ajouter des métadonnées de flux de travail.
La validation locale ciblée utilise des bases de données temporaires, des connexions séparées et un compteur de tentatives simulé, pas un fournisseur ou un message réel :
python3 -m unittest discover -s tests/skills -p 'test_approval_delivery_claims.py'.
Les tests nécessitent Python 3.11+ avec le support de sérialisation SQLite ; la référence n'utilise que la bibliothèque standard. Les vérifications de contrat de modèle de bureau existantes restent un contrôle de compatibilité distinct.
Auto-approbation temporisée (optionnelle)
Un brouillon peut porter auto_send_after (secondes depuis l'époque). Une tâche de balayage approuve les brouillons dont le délai est dépassé sans décision, en enregistrant l'opérateur auto-ttl, puis la livraison suit le chemin normal. Les actions de l'opérateur gagnent toujours : une décision change le statut avant que le balayage ne le voie, et une nouvelle soumission fait tourner l'horodatage et déplace ou efface le délai. Le balayage revérifie le statut et l'horodatage à l'intérieur de la transaction d'écriture, de sorte qu'une condition de course se résout en opération nulle. Les brouillons sans délai restent bloqués pour toujours.
Liaison de signal
Un brouillon peut nommer l'obligation entrante à laquelle il répond (signal_obligation_id). C'est le seul lien véridique pour la mesure de latence (signal entrant vers réponse rédigée) et permet à l'analyse SLA de considérer cet élément entrant comme traité. Rejeter le dépôt si la ligne référencée n'existe pas.
Exemples
Déposer un brouillon
file_request(
draft="Merci, nous pouvons garder le créneau jusqu'à vendredi. Quelle date de début vous conviendrait ?",
counterparty="fournisseur-acme",
context="réponse à la question sur la fenêtre de livraison",
origin_platform="slack", origin_channel="#acme-partagé",
origin_thread="1712345678.000100", priority="P1",
signal_obligation_id=412)
-> {obligation_id: 431, draft_sha256: "9f2c...", refiled: false}
La destination interne opérationnelle configurée et vérifiée voit :
Brouillon déposé pour approbation (P1, sha 9f2c8a1b). En attente de l'opérateur.
Le canal d'origine visible par la contrepartie ne reçoit aucun avis de dépôt. Si aucune destination interne vérifiée n'est disponible, l'accusé de réception reste dans le résultat de l'outil interne ou la surface opérateur, sans recours externe.
Redéposer après une réorientation
L'opérateur demande un brouillon plus court. Une nouvelle soumission pour la même (contrepartie, canal) renvoie refiled: true, le texte et l'empreinte du satellite changent, et updated_at avance. Une approbation cliquée sur l'ancienne ligne du panneau porte l'ancien horodatage et est ignorée.
Refus du garde-fou
DeskApprovalError: le garde-fou de base a refusé ce brouillon : le registre montre un
contrat signé pour 'fournisseur-acme'. Preuve : contrat principal exécuté
2026-08-14. Recommandation : ne pas demander. Redéposer avec
force_despite_signed_contract=true s'il s'agit véritablement d'un nouveau fil.
Pied de page de livraison dans un canal interne
Confirmé pour vendredi, date de début 2026-09-08.
approuvé par opérateur-a · accusé 118 · empreinte sha256 9f2c8a1b2d3e4f50
Invariants à tester
- Les accusés de réception de dépôt vont uniquement aux opérations internes configurées et vérifiées ; l'origine n'en reçoit un que lorsqu'elle est cette destination interne vérifiée.
- Une origine inconnue reste silencieuse. Une destination interne indisponible utilise le résultat de l'outil interne ou la surface opérateur, sans recours externe.
- Même (contrepartie, canal) déposé deux fois donne une seule obligation, deux horodatages.
- Une décision avec un horodatage périmé n'entraîne jamais de ligne de livraison.
- Deux revendicateurs concurrents donnent une seule autorisation d'envoi ; les perdants ne tentent jamais le transport.
- Les résultats inconnus et la persistance d'accusé de réception échouée n'activent jamais de réessai automatique.
- Une réalisation réussie enregistre l'accusé de réception et le statut envoyé en une seule transaction.
- Un horodatage, un texte, une empreinte ou une destination modifiés ne peuvent pas acquérir ou commencer une revendication.
- Les revendications actives bloquent une nouvelle soumission ; seule une annulation avant envoi peut libérer ce blocage.
- Le garde-fou indisponible ajoute le marqueur ; le garde-fou avec contrat signé refuse sans force.
- Auto-ttl ne se déclenche jamais contre un texte que l'opérateur a depuis redéposé.