Files
agents_and_robots/shell/audit/writer.go
T
egutierrez fb96a79feb feat: implementar audit trail con AuditWriter y emision de eventos
Crea shell/audit/ con Writer que escribe eventos de auditoria a archivo
JSONL y opcionalmente a un room Matrix. Integra la emision de eventos
en los puntos clave del runtime:

- message_received: al recibir cualquier evento Matrix (handler.go)
- command_exec: al ejecutar un comando (handler.go)
- tool_exec: al ejecutar una tool (tools/registry.go via AuditFunc callback)
- llm_request / llm_error: al llamar al LLM (llm.go)

El Writer se inicializa en agents/runtime.go si security.audit.enabled=true.
Usa patron de inyeccion de dependencias (MatrixSender como funcion,
AuditFunc como callback) para evitar acoplamiento entre packages.

Incluye tests completos para el Writer: escritura JSONL, filtrado por
Include, modo solo-file, modo solo-room, auto-set de timestamp.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:22:36 +00:00

134 lines
3.3 KiB
Go

// Package audit provides an audit event writer for compliance and review.
// Events are written to a JSONL file and/or sent to a Matrix room.
// This is fully impure (I/O): belongs in shell/.
package audit
import (
"encoding/json"
"fmt"
"log/slog"
"os"
"path/filepath"
"sync"
"time"
"github.com/enmanuel/agents/internal/config"
)
// Event types emitted by the audit trail.
const (
EventMessageReceived = "message_received"
EventCommandExec = "command_exec"
EventToolExec = "tool_exec"
EventLLMRequest = "llm_request"
EventLLMError = "llm_error"
)
// Event represents a single audit trail entry.
type Event struct {
Time time.Time `json:"time"`
AgentID string `json:"agent_id"`
EventType string `json:"event_type"`
SenderID string `json:"sender_id,omitempty"`
RoomID string `json:"room_id,omitempty"`
Detail string `json:"detail,omitempty"`
}
// MatrixSender is a function that sends a message to a Matrix room.
// Decouples audit from the Matrix client.
type MatrixSender func(roomID, msg string)
// Writer writes audit events to a JSONL file and/or a Matrix room.
type Writer struct {
cfg config.AuditCfg
sender MatrixSender // may be nil
logger *slog.Logger
include map[string]bool // allowlist of event types; empty = all
mu sync.Mutex
file *os.File
}
// New creates an AuditWriter from the given config.
// matrixSender may be nil if LogToRoom is not configured.
func New(cfg config.AuditCfg, sender MatrixSender, logger *slog.Logger) (*Writer, error) {
w := &Writer{
cfg: cfg,
sender: sender,
logger: logger.With("component", "audit"),
}
// Build include allowlist
if len(cfg.Include) > 0 {
w.include = make(map[string]bool, len(cfg.Include))
for _, t := range cfg.Include {
w.include[t] = true
}
}
// Open log file if configured
if cfg.LogFile != "" {
dir := filepath.Dir(cfg.LogFile)
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, fmt.Errorf("create audit log dir %s: %w", dir, err)
}
f, err := os.OpenFile(cfg.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
return nil, fmt.Errorf("open audit log %s: %w", cfg.LogFile, err)
}
w.file = f
}
return w, nil
}
// Emit writes an audit event. If the event type is not in the include list
// (when non-empty), the event is silently dropped. Thread-safe.
func (w *Writer) Emit(evt Event) {
// Filter by include allowlist (empty = pass all)
if len(w.include) > 0 && !w.include[evt.EventType] {
return
}
// Ensure time is set
if evt.Time.IsZero() {
evt.Time = time.Now().UTC()
}
// Write to JSONL file
if w.file != nil {
data, err := json.Marshal(evt)
if err != nil {
w.logger.Error("audit_marshal_error", "err", err)
return
}
data = append(data, '\n')
w.mu.Lock()
_, writeErr := w.file.Write(data)
w.mu.Unlock()
if writeErr != nil {
w.logger.Error("audit_write_error", "err", writeErr)
}
}
// Send to Matrix room
if w.sender != nil && w.cfg.LogToRoom != "" {
msg := fmt.Sprintf("**[audit]** `%s` | agent=%s sender=%s room=%s | %s",
evt.EventType, evt.AgentID, evt.SenderID, evt.RoomID, evt.Detail)
w.sender(w.cfg.LogToRoom, msg)
}
}
// Close closes the underlying log file.
func (w *Writer) Close() error {
w.mu.Lock()
defer w.mu.Unlock()
if w.file != nil {
return w.file.Close()
}
return nil
}