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
+27
View File
@@ -11,6 +11,7 @@ import (
coretypes "github.com/enmanuel/agents/pkg/llm"
"github.com/enmanuel/agents/pkg/orchestration"
"github.com/enmanuel/agents/pkg/sanitize"
"github.com/enmanuel/agents/shell/audit"
"github.com/enmanuel/agents/shell/bus"
)
@@ -25,6 +26,15 @@ func (a *Agent) handleEvent(ctx context.Context, msgCtx decision.MessageContext,
roomID := evt.RoomID.String()
// Audit: message_received
a.emitAudit(audit.Event{
AgentID: a.cfg.Agent.ID,
EventType: audit.EventMessageReceived,
SenderID: msgCtx.SenderID,
RoomID: roomID,
Detail: fmt.Sprintf("is_dm=%v is_mention=%v", msgCtx.IsDirectMsg, msgCtx.IsMention),
})
// Update room context for memory tools
a.roomCtx.Set(roomID)
@@ -59,6 +69,16 @@ func (a *Agent) handleEvent(ctx context.Context, msgCtx decision.MessageContext,
return
}
a.logger.Info("command_executed", "command", cmdName)
// Audit: command_exec
a.emitAudit(audit.Event{
AgentID: a.cfg.Agent.ID,
EventType: audit.EventCommandExec,
SenderID: msgCtx.SenderID,
RoomID: roomID,
Detail: fmt.Sprintf("command=%s", cmdName),
})
reply := handler(ctx, msgCtx)
_ = a.sendReply(ctx, roomID, msgCtx.EventID, msgCtx.ThreadID, reply)
return
@@ -341,6 +361,13 @@ func parseSeverity(s string) sanitize.Severity {
}
}
// emitAudit writes an audit event if the audit writer is enabled.
func (a *Agent) emitAudit(evt audit.Event) {
if a.auditWriter != nil {
a.auditWriter.Emit(evt)
}
}
// sanitizeInput runs prompt injection detection on the message content.
// Returns the (possibly modified) content and true if the message should be rejected.
func (a *Agent) sanitizeInput(content, roomID, senderID string) (string, bool) {