feat(server): endpoint POST /api/v1/agents/logs (push-log-DGX, brique 2)

Reçoit un batch de logs client, range via AgentLogsStore par machine_id.
Garde-fous : auth Bearer (401), agent actif via _guard_agent_registry_access
(403 si révoqué/inconnu, + touch_last_seen), cap anti-flood 413 (G3 Qwen,
RPA_AGENT_LOGS_MAX_BATCH=1000). TDD 4/4 ; non-régression enroll 16/16.

refs DETTE-020 DETTE-021

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-06-26 16:25:14 +02:00
parent a29b7a2f21
commit bbe897e614
2 changed files with 173 additions and 0 deletions

View File

@@ -583,6 +583,17 @@ _AGENTS_DB_PATH = os.environ.get(
)
agent_registry = AgentRegistry(db_path=_AGENTS_DB_PATH)
# push-log-DGX : store des logs poussés par les clients, rangés par machine_id
# (observabilité des postes sans AnyDesk — DETTE-020/021).
from .agent_logs_store import AgentLogsStore # noqa: E402
_AGENT_LOGS_DIR = os.environ.get(
"RPA_AGENT_LOGS_DIR", str(ROOT_DIR / "data" / "agent_logs")
)
# Garde-fou anti-flood (G3) : nb max d'entrées acceptées par batch.
_AGENT_LOGS_MAX_BATCH = int(os.environ.get("RPA_AGENT_LOGS_MAX_BATCH", "1000"))
agent_logs_store = AgentLogsStore(base_dir=_AGENT_LOGS_DIR)
def _agent_registry_has_entries() -> bool:
try:
@@ -1562,6 +1573,16 @@ class AgentUninstallRequest(BaseModel):
reason: Optional[str] = None
class AgentLogsRequest(BaseModel):
"""Batch de logs poussé par un client Léa (push-log-DGX).
`logs` = liste d'entrées {ts, level, logger, message} (format libre côté
serveur ; le client garantit le PII-safe avant push).
"""
machine_id: str
logs: list[dict] = []
# Thread de nettoyage périodique des replays terminés et sessions expirées
_cleanup_thread: Optional[threading.Thread] = None
_cleanup_running = False
@@ -7200,6 +7221,35 @@ async def agents_fleet():
}
@app.post("/api/v1/agents/logs")
async def agents_logs(request: AgentLogsRequest):
"""Réception des logs poussés par un client Léa (push-log-DGX).
Range les logs par machine_id (AgentLogsStore) pour consultation au
dashboard — diagnostic des postes sans AnyDesk. Mêmes garde-fous fleet
que stream/poll : un poste révoqué/inconnu est refusé (403).
"""
machine_id = (request.machine_id or "").strip()
if not machine_id:
raise HTTPException(status_code=400, detail="machine_id est obligatoire")
if len(request.logs) > _AGENT_LOGS_MAX_BATCH:
raise HTTPException(
status_code=413,
detail={
"error": "batch_too_large",
"max_batch": _AGENT_LOGS_MAX_BATCH,
"received": len(request.logs),
},
)
# Bloque les postes révoqués/désinstallés + met à jour last_seen_at.
_guard_agent_registry_access(machine_id, endpoint="agents/logs")
received = agent_logs_store.append(machine_id, request.logs)
return {"status": "ok", "received": received, "machine_id": machine_id}
# =========================================================================
# R2 MVP P0 — DialogResolver (catalogue centralisé des modaux runtime)
# Flag OFF par défaut. Activer en posant RPA_DIALOG_RESOLVER_ENABLED=true.