# 0035 — Observabilidad activa: audit trail + comando !metrics **Estado:** pendiente ## Objetivo Activar la infraestructura de auditoría (`AuditCfg`) que ya está definida en `internal/config/schema.go` pero nunca implementada, y añadir un comando `!metrics` que agregue datos del log del día actual. Ambas features usan infraestructura existente (JSONL logs, config schema, command system) sin dependencias nuevas. ## Contexto - `AuditCfg` lleva definida desde el schema original pero el código nunca la consume: no hay writer, no hay emisión de eventos, no hay integración con el runtime. - Los logs JSONL ya contienen métricas útiles (`duration_ms`, `tokens_used`, `tool_exec_*`, `command_received`) pero no hay forma de consultarlas sin parsear archivos manualmente. - `shell/logger/query.go` ya tiene helpers para leer y filtrar logs por fecha/campo. - El command system (`!status`, `!info`) ya existe y es extensible via built-in. ## Arquitectura ### Fase 1: Audit trail ``` shell/audit/ NEW — audit event writer (archivo JSONL + opcionalmente room Matrix) shell/audit/writer.go NEW — AuditWriter: escribe eventos a archivo y/o room ``` Integración en el runtime existente: ``` agents/handler.go MOD — emitir eventos audit en puntos clave agents/runtime.go MOD — inicializar AuditWriter si cfg.Security.Audit.Enabled tools/registry.go MOD — emitir evento audit en tool_exec ``` **Pure core / impure shell:** - No se añade nada a `pkg/` — los eventos audit son side effects puros (escritura a archivo/Matrix) - `shell/audit/` es 100% impuro: escribe a disco y opcionalmente envía mensajes Matrix ### Fase 2: Comando !metrics ``` agents/commands.go MOD — añadir cmdMetrics como built-in pkg/command/builtins.go MOD — añadir spec de !metrics ``` El comando lee los JSONL del día actual usando `shell/logger/query.go` (ya existente) y calcula agregados en memoria. No persiste nada, no crea tablas, no necesita SQLite. ## Tareas ### Fase 1 — Audit trail - [ ] **1.1** Crear `shell/audit/writer.go` con `AuditWriter` struct: - Constructor `New(cfg AuditCfg, matrixSender func(roomID, msg string), logger *slog.Logger) *AuditWriter` - Método `Emit(event AuditEvent)` que escribe a `LogFile` (JSONL append) y opcionalmente envía a `LogToRoom` (room Matrix) - `AuditEvent` struct: `{ Time, AgentID, EventType, SenderID, RoomID, Detail string }` - Filtrado por `Include` (lista de event types a auditar; vacío = todos) - Event types iniciales: `command_exec`, `tool_exec`, `llm_request`, `llm_error`, `message_received` - [ ] **1.2** Crear `shell/audit/writer_test.go`: - Test de escritura a archivo (verificar formato JSONL) - Test de filtrado por `Include` (solo emite los tipos configurados) - Test con `LogFile` vacío (no escribe a archivo, solo room) - Test con `LogToRoom` vacío (solo escribe a archivo) - [ ] **1.3** Integrar `AuditWriter` en `agents/runtime.go`: - En `New()`: si `cfg.Security.Audit.Enabled`, crear `AuditWriter` y guardarlo en el struct `Agent` - Pasar `matrixSender` como closure que usa el cliente Matrix del agente - Si audit no está habilitado, `AuditWriter` es nil (los call sites hacen nil-check) - [ ] **1.4** Emitir eventos en los puntos clave: - `agents/handler.go` → `message_received` (sender, room, is_dm) - `agents/handler.go` → `command_exec` (command name, sender) - `tools/registry.go` → `tool_exec` (tool name, duration, success/error) - `shell/llm/` → `llm_request` (provider, model, tokens) y `llm_error` (provider, error) ### Fase 2 — Comando !metrics - [ ] **2.1** Añadir spec en `pkg/command/builtins.go`: ```go {Name: "metrics", Description: "Métricas agregadas del día actual", Usage: "!metrics"} ``` - [ ] **2.2** Implementar `cmdMetrics` en `agents/commands.go`: - Leer logs del día actual con `logger.ReadDayLogs(logDir, agentID, time.Now())` - Calcular: total mensajes recibidos, comandos ejecutados, llamadas LLM (count + tokens totales + latencia media), llamadas a tools (count + errores), errores totales - Formatear como markdown table para Matrix - Ejemplo de output: ``` **Métricas de hoy (2026-04-09):** | Métrica | Valor | |---------|-------| | Mensajes recibidos | 42 | | Comandos ejecutados | 15 | | Llamadas LLM | 27 | | Tokens totales | 45,230 | | Latencia LLM media | 1,250 ms | | Tool calls | 8 | | Tool errors | 1 | | Errores totales | 2 | | Uptime | 6h 30m | ``` - [ ] **2.3** El handler necesita acceso al `logDir` del agente — pasar via config o campo en Agent struct (ya existe `a.cfg` con el agent ID, solo falta saber el baseDir de logs) ### Fase 3 — Tests y cleanup - [ ] **3.1** Tests para `cmdMetrics`: crear logs JSONL de ejemplo en tmpdir, verificar que los agregados son correctos - [ ] **3.2** Test de integración: `AuditWriter` + handler emite eventos reales a archivo temporal - [ ] **3.3** Documentar en `docs/security.md` la sección de audit trail (config YAML de ejemplo) ## Ejemplo de uso ### Audit trail Config en `agents/asistente-2/config.yaml`: ```yaml security: audit: enabled: true log_file: "logs/asistente-2/audit.jsonl" log_to_room: "!audit-room:matrix-af2f3d.organic-machine.com" include: - command_exec - tool_exec - llm_error ``` Resultado en `audit.jsonl`: ```json {"time":"2026-04-09T10:30:00Z","agent_id":"asistente-2","event":"command_exec","sender":"@user:matrix","room":"!abc:matrix","detail":"!status"} {"time":"2026-04-09T10:30:05Z","agent_id":"asistente-2","event":"tool_exec","sender":"@user:matrix","room":"!abc:matrix","detail":"http_get duration=350ms ok"} ``` ### Comando !metrics ``` Usuario: !metrics Bot: **Métricas de hoy (2026-04-09):** | Métrica | Valor | |---------|-------| | Mensajes recibidos | 42 | | Comandos ejecutados | 15 | | Llamadas LLM | 27 | | Tokens totales | 45,230 | | Latencia LLM media | 1,250 ms | | Tool calls | 8 | | Tool errors | 1 | | Errores totales | 2 | | Uptime | 6h 30m | ``` ## Decisiones de diseño 1. **Audit separado de logs normales**: los logs de runtime son para debugging (alto volumen, retención corta). El audit trail es para compliance/revisión (eventos selectivos, retención configurable independiente). 2. **Sin SQLite para métricas**: el comando `!metrics` calcula en memoria leyendo JSONL del día. Con los volúmenes actuales (~cientos de eventos/día por agente), esto es instantáneo. Si escala, se puede cachear o migrar a SQLite en un issue futuro. 3. **AuditWriter acepta matrixSender como función**: evita acoplar `shell/audit/` con el cliente Matrix directamente. Sigue el patrón de inyección de dependencias del proyecto. 4. **Include como allowlist**: lista vacía = auditar todo. Esto es deny-by-default invertido (opt-in por tipo de evento) para evitar audit logs gigantes. ## Prerequisitos Ninguno. Todo usa infraestructura existente: - `AuditCfg` en `internal/config/schema.go` - `shell/logger/query.go` para leer JSONL - `pkg/command/builtins.go` para registrar `!metrics` - `agents/commands.go` para implementar el handler ## Riesgos | Riesgo | Mitigación | |--------|------------| | Audit file crece sin límite | Usar el mismo `DailyRotatingWriter` de `shell/logger/` o rotación externa (logrotate) | | `LogToRoom` falla (room no existe) | Log warning y continuar — audit a archivo no debe fallar por Matrix | | `!metrics` lento con logs muy grandes | Los JSONL se rotan a 50MB max. Un día normal tiene KB-pocos MB. Aceptable. | | Audit de `message_received` loguea contenido sensible | El `Detail` solo incluye metadata (sender, room, is_dm), nunca el body del mensaje |