Boucle d'approbation opérateur
Un agent qui communique avec des contreparties externes ne doit jamais envoyer de messages de son propre chef et doit tenir l'opérateur informé en interne. Cette compétence définit le contrat : chaque brouillon sortant est enregistré comme une obligation, un opérateur décide du texte exact, et un registre de livraison prouve ce qui a été envoyé.
Quand l'utiliser
- Un agent rédige des réponses à des clients, fournisseurs, investisseurs ou partenaires dans un canal partagé, un email ou un chat, et un humain doit approuver avant l'envoi.
- Vous avez besoin d'une piste d'audit qui lie chaque message envoyé au texte exact du brouillon, à l'opérateur qui l'a approuvé et à l'heure de la décision.
- Vous avez observé qu'une approbation obsolète a libéré un brouillon réécrit, ou que deux travailleurs ont livré deux fois le même message approuvé.
- Les brouillons demandent à nouveau aux contreparties des faits que le registre contient déjà.
Comment ça fonctionne
Objets
| Objet | Signification |
|---|---|
| Obligation | Une chose que nous devons à une contrepartie. Le statut passe de rédigé, à approuvé ou rejeté, puis à envoyé. Porte la direction, la contrepartie, le canal et une époque updated_at. |
| Brouillon | Ligne satellite contenant le texte exact du brouillon, un sha256 de ce texte, les coordonnées d'origine (plateforme, canal, fil, utilisateur) et la priorité (P0 à P3). Un par obligation, remplacé lors d'un nouveau dépôt. |
| Décision | Approbation ou rejet d'un opérateur, enregistré avec l'identifiant de l'opérateur, un nonce et l'époque du brouillon sur lequel elle a été prise. |
| Instantané d'approbation | Texte immuable, hachage, époque et destination enregistrés par le scripteur de décision déjà autorisé. Les instantanés manquants ne peuvent pas accorder 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 une paire (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 dans les champs sur une seule ligne et appliquer les limites de longueur (brouillon, résumé, contexte, contrepartie). Les champs vides ou trop grands sont refusés, pas tronqués silencieusement.
- Exécuter la porte de contrôle pré-rédactionnelle (ci-dessous). Elle peut refuser le dépôt.
- Hacher le texte du brouillon avec sha256. Le préfixe du hachage va dans le résumé pour que le panneau d'approbation montre quel texte il approuve.
- Mise à jour. Si une obligation déjà rédigée et ouverte existe pour la même (contrepartie, canal), remplacer le satellite du brouillon et avancer le
updated_atde l'obligation. Cette avance est la rotation d'époque : toute décision indexée sur l'ancienne époque 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 ops 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 hachages des brouillons, le statut d'approbation, l'identité de l'opérateur et les métadonnées du workflow 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 personnes extérieures n'est pas une surface interne.
Le dépôt d'un brouillon n'autorise pas une réponse externe. Toute question de clarification ou réponse neutre permise par la politique est une décision sortante distincte, soumise aux contraintes existantes de mention, canal, brouillon uniquement, gelé et jamais dans la discipline de canal de contrepartie. Elle ne doit pas divulguer de métadonnées d'approbation internes.
Porte de contrôle pré-rédactionnelle
Avant de déposer un brouillon, interroger la ligne de base actuelle pour la contrepartie (un registre temporel, un magasin 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 la signature est la panne exacte que cette porte existe pour éviter.
- 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 porte injoignable : le dépôt continue et le contexte est marqué
[BASELINE_CHECK_UNAVAILABLE]. Le panneau voit que la garde était désactivée. Les pannes ne désactivent jamais silencieusement la porte. - Lorsque les faits sont disponibles, joindre les plus récents au contexte sous forme de résumé
[BASELINE FACTS: ...]pour que le brouillon atterrisse avec la vérité actuelle.
Décider
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'époque du brouillon (draft_updated_ts) et change le statut de l'obligation dans la même transaction. Une décision dont l'époque ne correspond pas au updated_at actuel est obsolète 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 décision et d'obligation, époque actuelle du brouillon, texte exact et SHA-256, plateforme/canal/fil et type draft_sent. Le scripteur de décision doit établir l'autorisation avant d'écrire ; la référence n'authentifie jamais un opérateur ni ne fabrique une décision. La politique d'approbation automatique n'est pas activée ou étendue par la référence. Les décisions héritées sans instantanés nécessitent une réconciliation explicite ou une nouvelle approbation ; ne jamais rétrocéder la permission à partir du brouillon mutable actuel.
Livraison
La référence SQLite est references/approval_claims.py. Elle accorde la permission 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 ou de migrations de production. Seul un scripteur de décision de confiance peut remplir les enregistrements d'approbation. Tous les scripteurs 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 en dehors 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'époque approuvée actuelle, 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 terminés ne peuvent pas être réclamés à nouveau. begin_dispatch()revalide la liaison et change atomiquementclaimedendispatchingen utilisant le jeton. Seul son appelant gagnant reçoit ledraft_textexact et la destination après le commit. Ne jamais régénérer le texte, relire un satellite mutable pour le transport ou réutiliser la charge utile pour une autre tentative. Une transaction d'appelant imbriquée est refusée ; la permission ne peut pas dépendre d'un commit ultérieur 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 l'obligation enenvoyé. Une complétion identique est un no-op ; les 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élais d'attente, la mort du travailleur après le début de l'envoi ou l'échec de la persistance de l'accusé de réception laissent une tentative bloquée.
mark_unknown()enregistre l'incertitude. Les revendications inconnues n'expirent jamais, ne se rouvrent pas, ne se réessayent pas automatiquement 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 des preuves de succès confirmé ; le module ne vérifie pas ces preuves. 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 le début de l'envoi mais avant le transport peut laisser zéro envoi et une revendication maintenue. Libérer un résultat inconnu pour une nouvelle tentative nécessiterait de cloisonner l'exécuteur d'origine et de vérifier la sémantique du fournisseur ; cette référence ne fournit délibérément aucune telle opération de nouvelle tentative.
| État de la revendication | États suivants autorisés |
|---|---|
| claimed | dispatching ou cancelled avant l'envoi |
| dispatching | delivered ou unknown |
| unknown | delivered uniquement via une réconciliation de confiance |
| delivered, cancelled | terminal ; la clé de décision ne peut pas être réutilisée |
Tant qu'une revendication est active, les gardes de la base de données bloquent 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 supprimés. Annuler une opération revendiquée avec son jeton avant de déposer à nouveau ; le jeton obsolète n'accorde alors rien. Après le début de l'envoi, retenir 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 vol.
Les décisions rejetées et les lignes héritées sans satellites/instantanés de brouillon 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 d'accusé de réception internes restent internes :
approuvé par <opérateur> · accusé <decision_id> · brouillon sha256 <prefix>.
Ne jamais modifier le texte externe déjà approuvé pour ajouter des métadonnées de workflow.
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 la sérialisation SQLite ; la référence utilise uniquement la bibliothèque standard. Les vérifications de contrat de modèle de bureau existantes restent une vérification de compatibilité distincte.
Approbation automatique limitée dans le temps (optionnelle)
Un brouillon peut porter auto_send_after (secondes d'époque). Une tâche de fond approuve les brouillons dont le délai est passé sans décision, en enregistrant l'opérateur auto-ttl, puis la livraison se déroule par le chemin normal. Les actions de l'opérateur gagnent toujours : une décision change le statut avant que la tâche ne le voie, et un nouveau dépôt fait pivoter l'époque et déplace ou efface le délai. La tâche revérifie le statut et l'époque à l'intérieur de la transaction d'écriture pour qu'une course se résolve comme un no-op. Les brouillons sans délai restent bloqués pour toujours.
Lien 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 au scan SLA de traiter cet élément entrant comme ayant reçu une réponse. 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 convient ?",
counterparty="fournisseur-acme",
context="réponse à la question sur la fenêtre de livraison",
origin_platform="slack", origin_channel="#acme-partage",
origin_thread="1712345678.000100", priority="P1",
signal_obligation_id=412)
-> {obligation_id: 431, draft_sha256: "9f2c...", refiled: false}
La destination interne ops 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 repli externe.
Re-dépôt après une orientation
L'opérateur demande un brouillon plus court. Le dépôt à nouveau pour la même (contrepartie, canal) retourne refiled: true, le texte et le hachage du satellite changent, et updated_at avance. Une approbation cliquée sur l'ancienne ligne du panneau porte l'ancienne époque et est ignorée.
Refus de la porte
DeskApprovalError: la porte de contrôle 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. Re-dé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 · brouillon sha256 9f2c8a1b2d3e4f50
Invariants à tester
- Les accusés de réception de dépôt vont uniquement vers les ops internes configurées et vérifiées ; l'origine en reçoit un seulement 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 repli externe.
- Même (contrepartie, canal) déposé deux fois donne une obligation, deux époques.
- Une décision avec une époque obsolète ne produit jamais de ligne de livraison.
- Deux réclamants simultanés donnent une permission d'envoi ; les perdants ne tentent jamais le transport.
- Les résultats inconnus et l'échec de persistance de l'accusé de réception n'activent jamais une nouvelle tentative automatique.
- La complétion réussie enregistre l'accusé de réception et le statut d'envoi dans une seule transaction.
- Une époque, un texte, un hachage ou une destination modifiée ne peut pas acquérir ou démarrer une revendication.
- Les réclamations actives bloquent le re-dépôt ; seule l'annulation avant l'envoi peut libérer ce blocage.
- La porte indisponible ajoute le marqueur ; la porte signée refuse sans forcer.
- Auto-ttl ne se déclenche jamais contre du texte que l'opérateur a depuis re-déposé.