package audit import ( "encoding/json" "fmt" "os" "path/filepath" "sync" "time" ) // AuditEvent represents an immutable audit log entry type AuditEvent struct { EventID string `json:"event_id"` EventType string `json:"event_type"` // "planner_decision", "judge_verdict", "implementer_change" WorkflowID string `json:"workflow_id"` TaskID string `json:"task_id"` Actor string `json:"actor"` // "planner", "judge", "implementer" Timestamp time.Time `json:"timestamp"` Action string `json:"action"` // Description of what was decided/done Reasoning string `json:"reasoning"` // Why this decision was made Input map[string]interface{} `json:"input,omitempty"` Output map[string]interface{} `json:"output,omitempty"` Status string `json:"status"` // "success", "failure", "pending" Error string `json:"error,omitempty"` Metadata map[string]interface{} `json:"metadata,omitempty"` } // AuditLogger logs immutable audit events type AuditLogger struct { mu sync.Mutex basePath string logFile string } // NewAuditLogger creates a new audit logger func NewAuditLogger(basePath string) *AuditLogger { return &AuditLogger{ basePath: basePath, logFile: filepath.Join(basePath, "audit", "audit.jsonl"), } } // LogEvent logs an audit event (immutable append-only) func (al *AuditLogger) LogEvent(event *AuditEvent) error { if event == nil { return fmt.Errorf("event cannot be nil") } al.mu.Lock() defer al.mu.Unlock() // Set timestamp if not already set if event.Timestamp.IsZero() { event.Timestamp = time.Now() } // Generate event ID if not set if event.EventID == "" { event.EventID = fmt.Sprintf("%s-%d", event.WorkflowID, event.Timestamp.UnixNano()) } // Create audit directory if it doesn't exist if err := os.MkdirAll(filepath.Dir(al.logFile), 0755); err != nil { return err } // Marshal to JSON data, err := json.Marshal(event) if err != nil { return err } // Append to file (immutable log) f, err := os.OpenFile(al.logFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) if err != nil { return err } defer f.Close() _, err = f.Write(append(data, '\n')) if err != nil { return err } return nil } // LogPlannerDecision logs a planner decision func (al *AuditLogger) LogPlannerDecision(workflowID, taskID string, decision string, reasoning string, metadata map[string]interface{}) error { event := &AuditEvent{ EventType: "planner_decision", WorkflowID: workflowID, TaskID: taskID, Actor: "planner", Timestamp: time.Now(), Action: decision, Reasoning: reasoning, Status: "success", Metadata: metadata, } return al.LogEvent(event) } // LogJudgeVerdict logs a judge verdict func (al *AuditLogger) LogJudgeVerdict(workflowID, taskID string, verdict string, reasoning string, metadata map[string]interface{}) error { event := &AuditEvent{ EventType: "judge_verdict", WorkflowID: workflowID, TaskID: taskID, Actor: "judge", Timestamp: time.Now(), Action: verdict, Reasoning: reasoning, Status: "success", Metadata: metadata, } return al.LogEvent(event) } // LogImplementerChange logs an implementer change func (al *AuditLogger) LogImplementerChange(workflowID, taskID string, changeDesc string, filesModified []string, metadata map[string]interface{}) error { output := map[string]interface{}{ "files_modified": filesModified, } event := &AuditEvent{ EventType: "implementer_change", WorkflowID: workflowID, TaskID: taskID, Actor: "implementer", Timestamp: time.Now(), Action: changeDesc, Output: output, Status: "success", Metadata: metadata, } return al.LogEvent(event) } // QueryByTask retrieves all events for a specific task func (al *AuditLogger) QueryByTask(taskID string) ([]*AuditEvent, error) { al.mu.Lock() defer al.mu.Unlock() data, err := os.ReadFile(al.logFile) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } var events []*AuditEvent var inLine []byte for _, ch := range data { if ch == '\n' { if len(inLine) > 0 { var event AuditEvent if err := json.Unmarshal(inLine, &event); err == nil { if event.TaskID == taskID { events = append(events, &event) } } } inLine = nil } else { inLine = append(inLine, ch) } } return events, nil } // QueryByWorkflow retrieves all events for a specific workflow func (al *AuditLogger) QueryByWorkflow(workflowID string) ([]*AuditEvent, error) { al.mu.Lock() defer al.mu.Unlock() data, err := os.ReadFile(al.logFile) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } var events []*AuditEvent var inLine []byte for _, ch := range data { if ch == '\n' { if len(inLine) > 0 { var event AuditEvent if err := json.Unmarshal(inLine, &event); err == nil { if event.WorkflowID == workflowID { events = append(events, &event) } } } inLine = nil } else { inLine = append(inLine, ch) } } return events, nil } // QueryByActor retrieves all events by a specific actor func (al *AuditLogger) QueryByActor(actor string) ([]*AuditEvent, error) { al.mu.Lock() defer al.mu.Unlock() data, err := os.ReadFile(al.logFile) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } var events []*AuditEvent var inLine []byte for _, ch := range data { if ch == '\n' { if len(inLine) > 0 { var event AuditEvent if err := json.Unmarshal(inLine, &event); err == nil { if event.Actor == actor { events = append(events, &event) } } } inLine = nil } else { inLine = append(inLine, ch) } } return events, nil } // QueryByTimeRange retrieves events within a time range func (al *AuditLogger) QueryByTimeRange(start, end time.Time) ([]*AuditEvent, error) { al.mu.Lock() defer al.mu.Unlock() data, err := os.ReadFile(al.logFile) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } var events []*AuditEvent var inLine []byte for _, ch := range data { if ch == '\n' { if len(inLine) > 0 { var event AuditEvent if err := json.Unmarshal(inLine, &event); err == nil { if event.Timestamp.After(start) && event.Timestamp.Before(end) { events = append(events, &event) } } } inLine = nil } else { inLine = append(inLine, ch) } } return events, nil } // GetAuditTrail retrieves the full audit trail func (al *AuditLogger) GetAuditTrail() ([]*AuditEvent, error) { al.mu.Lock() defer al.mu.Unlock() data, err := os.ReadFile(al.logFile) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } var events []*AuditEvent var inLine []byte for _, ch := range data { if ch == '\n' { if len(inLine) > 0 { var event AuditEvent if err := json.Unmarshal(inLine, &event); err == nil { events = append(events, &event) } } inLine = nil } else { inLine = append(inLine, ch) } } return events, nil } // GetEventCount returns the total number of audit events func (al *AuditLogger) GetEventCount() (int, error) { events, err := al.GetAuditTrail() if err != nil { return 0, err } return len(events), nil }