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>
This commit is contained in:
2026-04-09 20:13:21 +00:00
parent 892fe0cb19
commit fb96a79feb
7 changed files with 525 additions and 2 deletions
+63 -1
View File
@@ -22,6 +22,7 @@ import (
"github.com/enmanuel/agents/pkg/memory"
"github.com/enmanuel/agents/pkg/personality"
"github.com/enmanuel/agents/pkg/sanitize"
"github.com/enmanuel/agents/shell/audit"
"github.com/enmanuel/agents/shell/bus"
shellcron "github.com/enmanuel/agents/shell/cron"
"github.com/enmanuel/agents/shell/effects"
@@ -39,6 +40,14 @@ const (
defaultWindowSize = 20
)
// Option configures optional Agent behaviour.
type Option func(*Agent)
// WithLogDir sets the base directory for JSONL logs (used by !metrics command).
func WithLogDir(dir string) Option {
return func(a *Agent) { a.logDir = dir }
}
// CommandHandler executes a built-in command and returns the response text.
type CommandHandler func(ctx context.Context, msgCtx decision.MessageContext) string
@@ -97,12 +106,19 @@ type Agent struct {
// Scheduler — nil when no schedules are configured
scheduler *shellcron.Scheduler
// Audit writer — nil when audit is disabled
auditWriter *audit.Writer
// LogDir — base directory for JSONL logs (used by !metrics)
logDir string
}
// New assembles an Agent from its config, rules, pre-resolved ACL, and logger.
// The ACL is resolved externally (e.g. from security/ YAML files) and injected here.
// Pass acl.ACL{} (empty) for open access (no restrictions).
func New(cfg *config.AgentConfig, rules []decision.Rule, agentACL acl.ACL, logger *slog.Logger) (*Agent, error) {
// logDir is the base directory for JSONL logs (used by !metrics command); empty disables metrics.
func New(cfg *config.AgentConfig, rules []decision.Rule, agentACL acl.ACL, logger *slog.Logger, opts ...Option) (*Agent, error) {
// Matrix client
matrixClient, err := matrix.New(cfg.Matrix)
if err != nil {
@@ -177,6 +193,49 @@ func New(cfg *config.AgentConfig, rules []decision.Rule, agentACL acl.ACL, logge
roomCtx: roomCtx,
}
// Apply optional configuration
for _, opt := range opts {
opt(a)
}
// Initialize audit writer if enabled
if cfg.Security.Audit.Enabled {
var matrixSender audit.MatrixSender
if cfg.Security.Audit.LogToRoom != "" {
mc := matrixClient // capture for closure
matrixSender = func(roomID, msg string) {
if err := mc.SendMarkdown(context.Background(), roomID, msg); err != nil {
logger.Warn("audit_matrix_send_error", "room", roomID, "err", err)
}
}
}
aw, auditErr := audit.New(cfg.Security.Audit, matrixSender, logger)
if auditErr != nil {
logger.Error("audit_writer_init_failed", "err", auditErr)
} else {
a.auditWriter = aw
logger.Info("audit trail enabled",
"log_file", cfg.Security.Audit.LogFile,
"log_to_room", cfg.Security.Audit.LogToRoom,
"include", cfg.Security.Audit.Include,
)
// Wire tool_exec audit into the tool registry
agentID := cfg.Agent.ID
toolReg.SetAuditFunc(func(toolName string, durationMS int64, toolErr error) {
detail := fmt.Sprintf("tool=%s duration_ms=%d", toolName, durationMS)
if toolErr != nil {
detail += " error=" + toolErr.Error()
}
a.emitAudit(audit.Event{
AgentID: agentID,
EventType: audit.EventToolExec,
Detail: detail,
})
})
}
}
// Configure sanitization if enabled
if cfg.Security.Sanitize.Enabled {
minSev := parseSeverity(cfg.Security.Sanitize.MinSeverity)
@@ -318,6 +377,9 @@ func (a *Agent) Run(ctx context.Context) error {
if a.mcpManager != nil {
defer a.mcpManager.Close()
}
if a.auditWriter != nil {
defer a.auditWriter.Close()
}
a.logger.Info("agent starting",
"id", a.cfg.Agent.ID,
"name", a.cfg.Agent.Name,