fb96a79feb
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>
134 lines
3.3 KiB
Go
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
|
|
}
|