CI / test (push) Successful in 2m1s
- app/agent_journal.py : enveloppe ACC-0.2 signée HMAC-SHA256 (clé AGENT_EVENEMENTS_CLE, URL AGENT_EVENEMENTS_URL), envoi en arrière-plan, timeout 2 s, inactif sans clé, échecs avalés - main.py : _emettre_changement_colonne accolée aux log_event de déplacement (interface, étiquettes forge, webhook, réconciliation, PR fusionnée, branche) — kanbante.card.moved pour tout changement de colonne, kanbante.card.review_requested à l'entrée en Révision - config : AGENT_EVENEMENTS_URL/_CLE documentés et lus à l'appel - 247 tests verts (5 nouveaux : signature revérifiable, inactivité sans clé, moved, review_requested, panne du récepteur)
120 lines
3.5 KiB
Python
120 lines
3.5 KiB
Python
"""Émission d'événements vers le journal du service d'agents (Lot 2).
|
|
|
|
Kanbante émet ses changements de colonne au journal d'activité
|
|
cross-app du service d'agents, au contrat ACC-0.2 : enveloppe JSON
|
|
``{id, type, occurred_at, actor, domain, source_app,
|
|
object{ref, label, url}}`` signée HMAC-SHA256 du corps brut
|
|
(en-tête ``X-Draco-Signature``), avec la clé propre à Kanbante
|
|
(``AGENT_EVENEMENTS_CLE`` ; le service la connaît sous le nom
|
|
d'app « kanbante » dans ``AGENT_EVENEMENTS_CLES``).
|
|
|
|
Inactif tant que la clé n'est pas posée. Jamais bloquant, jamais
|
|
d'échec remonté au métier : l'envoi part dans un fil d'arrière-plan,
|
|
timeout court, exceptions avalées avec un avertissement au journal
|
|
local. L'appel sortant suit le patron de la maison (httpx, voir
|
|
``briefing.send_ntfy``).
|
|
"""
|
|
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import logging
|
|
import threading
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
|
|
import httpx
|
|
|
|
from app.config import get_settings
|
|
|
|
logger = logging.getLogger("kanbante")
|
|
|
|
SOURCE_APP = "kanbante"
|
|
DELAI_S = 2.0
|
|
|
|
|
|
def actif() -> bool:
|
|
"""True quand l'émission est configurée (clé propre posée)."""
|
|
return bool(get_settings().agent_evenements_cle)
|
|
|
|
|
|
def construire_enveloppe(
|
|
type_: str,
|
|
ref: str,
|
|
label: str,
|
|
url: str,
|
|
domain: str = "pro",
|
|
actor: str = "user",
|
|
) -> dict:
|
|
"""L'enveloppe ACC-0.2 : une référence et un libellé, jamais le
|
|
contenu de l'objet."""
|
|
return {
|
|
"id": uuid.uuid4().hex,
|
|
"type": type_,
|
|
"occurred_at": datetime.now(timezone.utc).isoformat(),
|
|
"actor": actor,
|
|
"domain": domain,
|
|
"source_app": SOURCE_APP,
|
|
"object": {"ref": ref, "label": label, "url": url},
|
|
}
|
|
|
|
|
|
def signer(corps: bytes, cle: str) -> str:
|
|
"""Signature ACC-0.2 : HMAC-SHA256 (hexadécimal) du corps brut."""
|
|
return hmac.new(cle.encode(), corps, hashlib.sha256).hexdigest()
|
|
|
|
|
|
def _poster(url: str, corps: bytes, signature: str) -> bool:
|
|
"""POST l'enveloppe signée ; False (et un avertissement) sur
|
|
tout échec — le récepteur en panne ne doit rien casser ici."""
|
|
try:
|
|
reponse = httpx.post(
|
|
url,
|
|
content=corps,
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"X-Draco-Signature": signature,
|
|
},
|
|
timeout=DELAI_S,
|
|
)
|
|
if reponse.status_code >= 300:
|
|
logger.warning(
|
|
"Journal agent : réponse %s de %s", reponse.status_code, url
|
|
)
|
|
return False
|
|
return True
|
|
except Exception as exc:
|
|
logger.warning("Journal agent injoignable (%s) : %s", url, exc)
|
|
return False
|
|
|
|
|
|
def emettre(
|
|
type_: str,
|
|
ref: str,
|
|
label: str,
|
|
url: str,
|
|
domain: str = "pro",
|
|
actor: str = "user",
|
|
) -> threading.Thread | None:
|
|
"""Émettre un événement, au mieux, depuis un fil d'arrière-plan.
|
|
|
|
Retourne le fil lancé (les tests peuvent le joindre), ou None
|
|
quand l'émission n'est pas configurée. Ne lève jamais.
|
|
"""
|
|
settings = get_settings()
|
|
cle = settings.agent_evenements_cle
|
|
if not cle:
|
|
return None
|
|
enveloppe = construire_enveloppe(
|
|
type_, ref, label, url, domain=domain, actor=actor
|
|
)
|
|
corps = json.dumps(enveloppe, ensure_ascii=False).encode("utf-8")
|
|
fil = threading.Thread(
|
|
target=_poster,
|
|
args=(settings.agent_evenements_url, corps, signer(corps, cle)),
|
|
name="kanbante-agent-journal",
|
|
daemon=True,
|
|
)
|
|
fil.start()
|
|
return fil
|