feat: add audit trail for detach operations (gt-h6eq.8)
Add DetachMoleculeWithAudit() function that logs detach operations to .beads/audit.log in JSONL format. Each entry captures: - timestamp - operation type (detach, burn, squash) - pinned_bead_id - detached_molecule - detached_by (agent identity) - reason (optional) - previous_state Updated callers: - runMoleculeDetach: logs "detach" operation - runMoleculeBurn: logs "burn" operation with reason - runMoleculeSquash: logs "squash" operation with digest ID 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,9 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -1007,3 +1009,92 @@ func (b *Beads) GetAttachment(pinnedBeadID string) (*AttachmentFields, error) {
|
||||
func currentTimestamp() string {
|
||||
return time.Now().UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// DetachAuditEntry represents an audit log entry for a detach operation.
|
||||
type DetachAuditEntry struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
Operation string `json:"operation"` // "detach", "burn", "squash"
|
||||
PinnedBeadID string `json:"pinned_bead_id"`
|
||||
DetachedMolecule string `json:"detached_molecule"`
|
||||
DetachedBy string `json:"detached_by,omitempty"` // Agent that triggered detach
|
||||
Reason string `json:"reason,omitempty"` // Optional reason for detach
|
||||
PreviousState string `json:"previous_state,omitempty"`
|
||||
}
|
||||
|
||||
// DetachOptions specifies optional context for a detach operation.
|
||||
type DetachOptions struct {
|
||||
Operation string // "detach", "burn", "squash" - defaults to "detach"
|
||||
Agent string // Who is performing the detach
|
||||
Reason string // Optional reason for the detach
|
||||
}
|
||||
|
||||
// DetachMoleculeWithAudit removes molecule attachment from a pinned bead and logs the operation.
|
||||
// Returns the updated issue.
|
||||
func (b *Beads) DetachMoleculeWithAudit(pinnedBeadID string, opts DetachOptions) (*Issue, error) {
|
||||
// Fetch the pinned bead first to get previous state
|
||||
issue, err := b.Show(pinnedBeadID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching pinned bead: %w", err)
|
||||
}
|
||||
|
||||
// Get current attachment info for audit
|
||||
attachment := ParseAttachmentFields(issue)
|
||||
if attachment == nil {
|
||||
return issue, nil // Nothing to detach
|
||||
}
|
||||
|
||||
// Log the detach operation
|
||||
operation := opts.Operation
|
||||
if operation == "" {
|
||||
operation = "detach"
|
||||
}
|
||||
entry := DetachAuditEntry{
|
||||
Timestamp: currentTimestamp(),
|
||||
Operation: operation,
|
||||
PinnedBeadID: pinnedBeadID,
|
||||
DetachedMolecule: attachment.AttachedMolecule,
|
||||
DetachedBy: opts.Agent,
|
||||
Reason: opts.Reason,
|
||||
PreviousState: issue.Status,
|
||||
}
|
||||
if err := b.LogDetachAudit(entry); err != nil {
|
||||
// Log error but don't fail the detach operation
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to write audit log: %v\n", err)
|
||||
}
|
||||
|
||||
// Clear attachment fields by passing nil
|
||||
newDesc := SetAttachmentFields(issue, nil)
|
||||
|
||||
// Update the issue
|
||||
if err := b.Update(pinnedBeadID, UpdateOptions{Description: &newDesc}); err != nil {
|
||||
return nil, fmt.Errorf("updating pinned bead: %w", err)
|
||||
}
|
||||
|
||||
// Re-fetch to return updated state
|
||||
return b.Show(pinnedBeadID)
|
||||
}
|
||||
|
||||
// LogDetachAudit appends an audit entry to the audit log file.
|
||||
// The audit log is stored in .beads/audit.log as JSONL format.
|
||||
func (b *Beads) LogDetachAudit(entry DetachAuditEntry) error {
|
||||
auditPath := filepath.Join(b.workDir, ".beads", "audit.log")
|
||||
|
||||
// Marshal entry to JSON
|
||||
data, err := json.Marshal(entry)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling audit entry: %w", err)
|
||||
}
|
||||
|
||||
// Append to audit log file
|
||||
f, err := os.OpenFile(auditPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening audit log: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := f.Write(append(data, '\n')); err != nil {
|
||||
return fmt.Errorf("writing audit entry: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user