Aucun nom public d'une autre app dracodev n'est codé en dur dans le code vivant : nouveau module app/services/draco_urls.py (env > config > défaut officiel, jamais de -dev en défaut). CSP connect-src/frame-src, bouton SSO ObsiGate, révocations SSO, événements agent, preset MCP TikTak, barre commune, panneau Spark (injection hôte DRACO_PANNEAU_CONFIG), refs croisées (hôte configuré + officiel reconnus, URL d'affichage reconstruite, aucune donnée stockée migrée), User-Agent OG, liens Gitea. Variables ajoutées DRACO_AGENT_URL/TIKTAK_URL/KANBANTE_URL/FLOWDECK_URL, réutilisées DRACO_SSO_URL/OAUTH_REDIRECT_URI/AGENT_EVENEMENTS_URL/GITEA_URL/APP_BASE_URL. Version 7.82.0.
133 lines
4.5 KiB
Python
133 lines
4.5 KiB
Python
"""Émission d'événements vers le journal du service d'agents
|
|
(Fusion, Lot 2 — contrat ACC-0.2).
|
|
|
|
Flowdeck émet ses mutations significatives (pages et collections)
|
|
vers `POST /api/v1/evenements` du service d'agents, qui les expose
|
|
dans le « Journal d'aujourd'hui » de la console. L'enveloppe ne
|
|
contient qu'une référence, un libellé et un lien — jamais le
|
|
contenu de l'objet : {id, type, occurred_at, actor, domain,
|
|
source_app, object{ref, label, url}}.
|
|
|
|
Authentification : signature HMAC-SHA256 du corps brut, en-tête
|
|
`X-Draco-Signature`, avec la clé propre à Flowdeck
|
|
(`AGENT_EVENEMENTS_CLE`, jamais en dur). Sans clé, le module est
|
|
INACTIF : aucun appel, aucun bruit.
|
|
|
|
Discipline « au mieux » (même esprit que le journal du service) :
|
|
l'envoi part dans un fil d'arrière-plan démon, délai maximal 2 s,
|
|
et toute erreur est avalée avec une ligne de journal — une panne
|
|
du récepteur ne doit jamais casser ni ralentir le métier.
|
|
|
|
Aucun événement « meeting » n'est émis ici : la décision A/B de
|
|
Bruno sur le bloc meeting de Flowdeck est toujours ouverte.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import logging
|
|
import os
|
|
import threading
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
|
|
import httpx
|
|
|
|
log = logging.getLogger("flowdeck.evenements")
|
|
|
|
SOURCE_APP = "flowdeck"
|
|
DELAI_S = 2.0
|
|
|
|
|
|
def _cle() -> str:
|
|
return os.environ.get("AGENT_EVENEMENTS_CLE", "").strip()
|
|
|
|
|
|
def _url() -> str:
|
|
# AGENT_EVENEMENTS_URL (en service) prime ; à défaut, l'endpoint
|
|
# est dérivé de la base de l'agent résolue par la config
|
|
# (DRACO_AGENT_URL, défaut = nom officiel) — aucun nom figé.
|
|
from app.services import draco_urls
|
|
|
|
return draco_urls.agent_evenements_url()
|
|
|
|
|
|
def _url_defaut() -> str:
|
|
return _url()
|
|
|
|
|
|
# Compat : ancien nom lu par d'éventuels consommateurs externes.
|
|
URL_DEFAUT = _url()
|
|
|
|
|
|
def actif() -> bool:
|
|
"""L'émission n'existe que si la clé de Flowdeck est configurée."""
|
|
return bool(_cle())
|
|
|
|
|
|
def url_page(page_id: int) -> str:
|
|
"""Lien profond d'une page d'espace de travail (vue HTML du board)."""
|
|
return f"/board/pages/{page_id}"
|
|
|
|
|
|
def url_collection(collection_id: int) -> str:
|
|
"""Lien profond d'une collection (sa vue tableau). Une page de
|
|
collection n'a pas d'URL autonome : elle s'ouvre dans cette vue."""
|
|
return f"/db/{collection_id}/view/table"
|
|
|
|
|
|
def construire_enveloppe(type_: str, objet_ref: str, label: str,
|
|
url: str = "/", domain: str = "pro",
|
|
actor: str | None = None) -> tuple[dict, bytes]:
|
|
"""L'enveloppe ACC-0.2 et son corps JSON exact (celui qui est
|
|
signé puis envoyé — la signature porte sur ces octets-là)."""
|
|
env = {
|
|
"id": uuid.uuid4().hex,
|
|
"type": type_,
|
|
"occurred_at": datetime.now(timezone.utc).isoformat(),
|
|
"actor": actor or "user",
|
|
"domain": domain or "pro",
|
|
"source_app": SOURCE_APP,
|
|
"object": {"ref": objet_ref, "label": label or "", "url": url or "/"},
|
|
}
|
|
corps = json.dumps(env, ensure_ascii=False).encode("utf-8")
|
|
return env, corps
|
|
|
|
|
|
def _signer(corps: bytes) -> str:
|
|
return hmac.new(_cle().encode("utf-8"), corps, hashlib.sha256).hexdigest()
|
|
|
|
|
|
def _envoyer(corps: bytes) -> None:
|
|
"""Le POST signé, dans le fil d'arrière-plan. Avale tout : le
|
|
journal distant est un confort, jamais une dépendance."""
|
|
try:
|
|
with httpx.Client(timeout=DELAI_S, trust_env=False) as client:
|
|
client.post(
|
|
_url(), content=corps,
|
|
headers={"Content-Type": "application/json",
|
|
"X-Draco-Signature": _signer(corps)},
|
|
)
|
|
except Exception as exc: # panne, délai, DNS… : on log et on passe
|
|
log.warning("événement non livré au journal du service (%s)", exc)
|
|
|
|
|
|
def emettre(type_: str, objet_ref: str, label: str, url: str = "/",
|
|
domain: str = "pro", actor: str | None = None) -> bool:
|
|
"""Émet un événement en tâche de fond. Retourne True si l'envoi
|
|
a été déclenché, False si le module est inactif (pas de clé) ou
|
|
si l'enveloppe n'a pas pu être construite. Ne lève jamais."""
|
|
if not actif():
|
|
return False
|
|
try:
|
|
_env, corps = construire_enveloppe(type_, objet_ref, label, url,
|
|
domain, actor)
|
|
except Exception:
|
|
log.warning("événement %s non construit", type_, exc_info=True)
|
|
return False
|
|
threading.Thread(target=_envoyer, args=(corps,), daemon=True,
|
|
name="flowdeck-evenements").start()
|
|
return True
|