# 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 |
