Files
gastown/internal/beads/audit.go
max 1b69576573 fix: Address golangci-lint errors (errcheck, gosec) (#76)
Apply PR #76 from dannomayernotabot:

- Add golangci exclusions for internal package false positives
- Tighten file permissions (0644 -> 0600) for sensitive files
- Add ReadHeaderTimeout to HTTP server (slowloris prevention)
- Explicit error ignoring with _ = for intentional cases
- Add //nolint comments with justifications
- Spelling: cancelled -> canceled (US locale)

Co-Authored-By: dannomayernotabot <noreply@github.com>

🤖 Generated with Claude Code
2026-01-03 16:11:55 -08:00

99 lines
3.1 KiB
Go

// Package beads provides audit logging for molecule operations.
package beads
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
// 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, 0600) //nolint:gosec // G304: path is constructed internally
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
}