package audit import ( "context" "log/slog" "time" "ai-operator/internal/audit/redaction" "ai-operator/internal/config" ) type Service struct { repo Repository cfg config.AuditConfig redactor redaction.DefaultRedactor logger *slog.Logger } func NewService(repo Repository, cfg config.AuditConfig, logger *slog.Logger) *Service { if repo == nil || !cfg.Enabled { repo = NoopRepository{} } return &Service{repo: repo, cfg: cfg, redactor: redaction.New(), logger: logger} } func (s *Service) Health(ctx context.Context) error { return s.repo.Health(ctx) } func (s *Service) UpsertCall(ctx context.Context, c CallRecord) error { c.CallerNumberMasked = s.redact(c.CallerNumberMasked, s.cfg.MaxEventMetadataChars) c.Metadata = s.redactJSON(c.Metadata) return s.handle("audit upsert call", s.repo.UpsertCall(ctx, c)) } func (s *Service) EndCall(ctx context.Context, callID, reason string) error { return s.handle("audit end call", s.repo.EndCall(ctx, callID, s.redact(reason, 512))) } func (s *Service) AddEvent(ctx context.Context, e EventRecord) error { e.Message = s.redact(e.Message, s.cfg.MaxEventMetadataChars) e.Metadata = s.redactJSON(e.Metadata) return s.handle("audit event", s.repo.AddEvent(ctx, e)) } func (s *Service) AddTranscript(ctx context.Context, t TranscriptRecord) error { if !s.cfg.StoreTranscripts { return nil } if t.EventType == "transcript.user.delta" || t.EventType == "transcript.assistant.delta" { if !s.cfg.StoreTranscriptDeltas { return nil } } t.Text = s.redact(t.Text, s.cfg.MaxTranscriptChars) t.Metadata = s.redactJSON(t.Metadata) return s.handle("audit transcript", s.repo.AddTranscript(ctx, t)) } func (s *Service) AddToolAudit(ctx context.Context, t ToolAuditRecord) error { t.Args = s.redactJSON(t.Args) t.Result = s.redactJSON(t.Result) t.Metadata = s.redactJSON(t.Metadata) return s.handle("audit tool", s.repo.AddToolAudit(ctx, t)) } func (s *Service) AddKBAudit(ctx context.Context, k KBAuditRecord) error { k.Query = s.redact(k.Query, s.cfg.MaxTranscriptChars) k.Metadata = s.redactJSON(k.Metadata) return s.handle("audit kb", s.repo.AddKBAudit(ctx, k)) } func (s *Service) AddHandoffAudit(ctx context.Context, h HandoffAuditRecord) error { h.Target = s.redact(h.Target, 512) h.Summary = s.redact(h.Summary, s.cfg.MaxEventMetadataChars) h.Metadata = s.redactJSON(h.Metadata) return s.handle("audit handoff", s.repo.AddHandoffAudit(ctx, h)) } func (s *Service) AddProviderAudit(ctx context.Context, p ProviderAuditRecord) error { if !s.cfg.StoreProviderEvents { return nil } p.Error = s.redact(p.Error, s.cfg.MaxEventMetadataChars) p.Metadata = s.redactJSON(p.Metadata) return s.handle("audit provider", s.repo.AddProviderAudit(ctx, p)) } func (s *Service) AddMediaAudit(ctx context.Context, m MediaAuditRecord) error { if !s.cfg.StoreMediaStats { return nil } m.Error = s.redact(m.Error, s.cfg.MaxEventMetadataChars) m.Metadata = s.redactJSON(m.Metadata) return s.handle("audit media", s.repo.AddMediaAudit(ctx, m)) } func (s *Service) ExportCall(ctx context.Context, callID string) (CallAuditExport, error) { return s.repo.ExportCall(ctx, callID) } func (s *Service) Prune(ctx context.Context, req RetentionPruneRequest) (RetentionPruneResult, error) { if req.Now.IsZero() { req.Now = time.Now().UTC() } if req.RetentionDays == 0 { req.RetentionDays = s.cfg.RetentionDays } if req.TranscriptRetentionDays == 0 { req.TranscriptRetentionDays = s.cfg.TranscriptRetentionDays } if req.ToolAuditRetentionDays == 0 { req.ToolAuditRetentionDays = s.cfg.ToolAuditRetentionDays } if req.ErrorAuditRetentionDays == 0 { req.ErrorAuditRetentionDays = s.cfg.ErrorAuditRetentionDays } return s.repo.Prune(ctx, req) } func (s *Service) redact(v string, max int) string { if max > 0 && len([]rune(v)) > max { r := []rune(v) v = string(r[:max]) } if !s.cfg.RedactionEnabled { return v } return s.redactor.RedactText(v).Text } func (s *Service) redactJSON(v map[string]any) map[string]any { if v == nil { return map[string]any{} } if !s.cfg.RedactionEnabled { return v } return s.redactor.RedactJSON(v) } func (s *Service) handle(msg string, err error) error { if err == nil { return nil } if s.logger != nil { s.logger.Warn(msg, "error", s.redact(err.Error(), 1024)) } if s.cfg.FailClosed { return err } return nil }