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:
+63
-1
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user