Remove deprecated audit endpoint

- Remove audit endpoint routes from 9P filesystem
- Delete audit handler functions (readAuditLog, GetAuditLog)
- Remove audit package and all audit logging code
- Remove audit log viewer from TUI (anvillm)
- Remove audit log streaming from web UI (anvilweb)
- Update documentation to reference events system instead
- All audit functionality replaced by events system
This commit is contained in:
Levi Neely 2026-02-20 22:18:53 +01:00
parent d5eac1d2c1
commit c6987a7962
7 changed files with 5 additions and 306 deletions

View File

@ -37,7 +37,7 @@ agent/beads/
├── dependencies-meta # Dependencies with metadata (JSON)
├── dependents-meta # Dependents with metadata (JSON)
├── tree # Dependency tree (JSON)
└── events # Audit trail events (JSON)
└── events # Event history (JSON)
```
## Control Commands
@ -123,7 +123,7 @@ echo "complete bd-a1b2" | 9p write agent/beads/ctl
# Read dependency tree
9p read agent/beads/bd-a1b2/tree
# Read audit trail
# Read event history
9p read agent/beads/bd-a1b2/events
# Add dependency (parent blocks child)
@ -208,7 +208,7 @@ anvilmcp exposes beads operations as MCP tools (calls `9p write agent/beads/ctl`
- **Crash resilience** — Beads persist in Dolt database
- **Resumability** — Agents pick up where others left off
- **Dependency tracking** — Automatic blocking relationships
- **Version control** — Full audit trail via Dolt
- **Version control** — Full history via Dolt
- **Git portability** — JSONL export syncs via git
- **Scriptability** — Standard file operations
- **Pure Go** — No Python dependency

View File

@ -77,6 +77,6 @@ Backend sessions run in tmux windows with:
Potential security enhancements for consideration:
1. Add authentication to 9P protocol (shared secret, capabilities)
2. Per-session access control lists
3. Audit logging of 9P operations
3. Enhanced event logging of 9P operations
4. Encrypted transport for 9P (though Unix sockets are local-only)
5. Namespace isolation via Linux namespaces or FreeBSD jails

View File

@ -116,9 +116,6 @@ func setupUI() {
case 'a':
showAliasDialog()
return nil
case 'l':
showLogViewer()
return nil
case 'd':
showDaemonStatus()
return nil
@ -585,63 +582,6 @@ func killSelectedSession() {
}
}
func showLogViewer() {
// Read the centralized audit log
path := "audit"
logData, err := readLogFile(path)
if err != nil {
updateStatus(fmt.Sprintf("[red]Error reading audit log: %v", err))
return
}
logText := string(logData)
if logText == "" {
logText = "[gray]No audit log entries yet[-]"
}
textView := tview.NewTextView().
SetDynamicColors(true).
SetText(logText).
SetScrollable(true).
SetWordWrap(true)
textView.SetBorder(true).
SetTitle(" Audit Log ").
SetTitleAlign(tview.AlignLeft)
// Scroll to bottom
textView.ScrollToEnd()
textView.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Rune() {
case 'q':
pages.RemovePage("log")
return nil
case 'r':
// Refresh log
logData, err := readLogFile(path)
if err != nil {
updateStatus(fmt.Sprintf("[red]Error refreshing log: %v", err))
return nil
}
logText := string(logData)
if logText == "" {
logText = "[gray]No audit log entries yet[-]"
}
textView.SetText(logText)
textView.ScrollToEnd()
return nil
}
if event.Key() == tcell.KeyEscape {
pages.RemovePage("log")
return nil
}
return event
})
pages.AddPage("log", createModalDynamic(textView, 8, 25), true, true)
}
func showDaemonStatus() {
// TODO: Implement daemon status view
updateStatus("[yellow]Daemon status view not yet implemented")

View File

@ -51,7 +51,6 @@ func main() {
http.Handle("/", http.FileServer(http.FS(staticFS)))
http.HandleFunc("/api/sessions", handleSessions)
http.HandleFunc("/api/session/", handleSession)
http.HandleFunc("/api/log", handleLogStream)
http.HandleFunc("/api/inbox", handleInbox)
http.HandleFunc("/api/inbox/count", handleInboxCount)
http.HandleFunc("/api/inbox/reply", handleInboxReply)
@ -501,65 +500,6 @@ func updateSessionFile(w http.ResponseWriter, r *http.Request, id, file string)
w.WriteHeader(http.StatusOK)
}
func handleLogStream(w http.ResponseWriter, r *http.Request) {
// Stream the centralized audit log
// Set headers for SSE
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
fs, err := connect()
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
defer fs.Close()
// Open audit log file for streaming
fid, err := fs.Open("audit", plan9.OREAD)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer fid.Close()
// Flush immediately to establish connection
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
// Stream log data
buf := make([]byte, 4096)
for {
n, err := fid.Read(buf)
if n > 0 {
// Escape data for SSE format - each line must be prefixed with "data: "
data := string(buf[:n])
lines := strings.Split(data, "\n")
for _, line := range lines {
if line != "" || len(lines) > 1 {
fmt.Fprintf(w, "data: %s\n", line)
}
}
fmt.Fprintf(w, "\n")
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
}
if err != nil {
// Connection closed or error
break
}
// Check if client disconnected
select {
case <-r.Context().Done():
return
default:
}
}
}
type Message struct {
ID string `json:"id"`
From string `json:"from"`

View File

@ -1,112 +0,0 @@
package audit
import (
"fmt"
"strings"
"sync"
"time"
)
const (
maxSize = 64 * 1024 * 1024 // 64MB
trimPercent = 25 // Remove 25% when limit reached
)
// Log maintains a centralized audit log of all messages
type Log struct {
entries []string
totalBytes int
mu sync.RWMutex
notifyCh chan struct{}
}
// NewLog creates a new audit log
func NewLog() *Log {
return &Log{
entries: make([]string, 0, 1000),
notifyCh: make(chan struct{}, 1),
}
}
// Append adds a new entry to the audit log
// Format: {timestamp} {message-type} {sender}->{recipient} {subject}: {body}
func (l *Log) Append(msgType, sender, recipient, subject, body string) {
l.mu.Lock()
defer l.mu.Unlock()
// Format timestamp
timestamp := time.Now().Format(time.RFC3339)
// Escape newlines in subject and body for single-line grep/awk
// But for human readability, we'll use multi-line format with blank line separator
entry := fmt.Sprintf("%s %s %s->%s\nSubject: %s\n%s\n",
timestamp, msgType, sender, recipient, subject, body)
l.entries = append(l.entries, entry)
l.totalBytes += len(entry)
// Trim if needed
if l.totalBytes > maxSize {
l.trim()
}
// Notify waiters
select {
case l.notifyCh <- struct{}{}:
default:
}
}
// trim removes the oldest 25% of entries
func (l *Log) trim() {
if len(l.entries) == 0 {
return
}
removeCount := len(l.entries) * trimPercent / 100
if removeCount == 0 {
removeCount = 1
}
// Calculate bytes to remove
removedBytes := 0
for i := 0; i < removeCount && i < len(l.entries); i++ {
removedBytes += len(l.entries[i])
}
// Remove entries
l.entries = l.entries[removeCount:]
l.totalBytes -= removedBytes
}
// Read returns the entire audit log as a string
func (l *Log) Read() string {
l.mu.RLock()
defer l.mu.RUnlock()
return strings.Join(l.entries, "\n")
}
// ReadFrom returns audit log content from a specific byte offset
func (l *Log) ReadFrom(offset int64) (string, bool) {
l.mu.RLock()
defer l.mu.RUnlock()
fullLog := strings.Join(l.entries, "\n")
if offset >= int64(len(fullLog)) {
return "", false
}
return fullLog[offset:], true
}
// WaitForData returns a channel that signals when new data is available
func (l *Log) WaitForData() <-chan struct{} {
return l.notifyCh
}
// Size returns the current size in bytes
func (l *Log) Size() int {
l.mu.RLock()
defer l.mu.RUnlock()
return l.totalBytes
}

View File

@ -1,7 +1,6 @@
package mailbox
import (
"anvillm/internal/audit"
"fmt"
"sync"
"time"
@ -20,7 +19,6 @@ type Manager struct {
completed map[string][]*Message
mu sync.RWMutex
idCounter uint64
auditLog *audit.Log
sessions SessionGetter
onSend func(senderID string, msg *Message)
onRecv func(receiverID string, msg *Message)
@ -32,7 +30,6 @@ func NewManager() *Manager {
inboxes: make(map[string][]*Message),
outboxes: make(map[string][]*Message),
completed: make(map[string][]*Message),
auditLog: audit.NewLog(),
}
// Initialize user mailbox
m.inboxes["user"] = []*Message{}
@ -147,13 +144,6 @@ func (m *Manager) DeliverToInbox(sessionID string, msg *Message) error {
m.inboxes[sessionID] = append(m.inboxes[sessionID], msg)
// Format sender and receiver with aliases
sender := m.formatParticipant(msg.From)
receiver := m.formatParticipant(msg.To)
// Audit log the delivery
m.auditLog.Append(string(msg.Type), sender, receiver, msg.Subject, msg.Body)
if m.onRecv != nil {
m.onRecv(sessionID, msg)
}
@ -307,7 +297,4 @@ func (m *Manager) MoveToCompleted(sessionID string, msg *Message) {
m.completed[sessionID] = append(m.completed[sessionID], msg)
}
// GetAuditLog returns the audit log
func (m *Manager) GetAuditLog() *audit.Log {
return m.auditLog
}

View File

@ -68,7 +68,6 @@ const (
qidCtl
qidList
qidStatus
qidAudit // agent/audit
qidEvents // agent/events
qidUser // user directory
qidUserInbox // user/inbox
@ -287,9 +286,6 @@ func (s *Server) walk(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
case "list":
qid = plan9.Qid{Type: QTFile, Path: qidList}
newPath = "/list"
case "audit":
qid = plan9.Qid{Type: QTFile, Path: qidAudit}
newPath = "/audit"
case "events":
qid = plan9.Qid{Type: QTFile, Path: qidEvents}
newPath = "/events"
@ -434,9 +430,6 @@ func (s *Server) read(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
if f.qid.Type&QTDir != 0 {
data = s.readDir(f.path, fc.Offset, fc.Count)
} else if f.path == "/audit" {
// Streaming audit log (like tail -f)
data = s.readAuditLog(fc.Offset, fc.Count)
} else {
content := s.readFile(f.path)
if fc.Offset < uint64(len(content)) {
@ -865,10 +858,6 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte {
Qid: plan9.Qid{Type: QTFile, Path: qidStatus}, Mode: 0444, Name: "status",
Uid: "q", Gid: "q", Muid: "q",
})
dirs = append(dirs, plan9.Dir{
Qid: plan9.Qid{Type: QTFile, Path: qidAudit}, Mode: 0444, Name: "audit",
Uid: "q", Gid: "q", Muid: "q",
})
dirs = append(dirs, plan9.Dir{
Qid: plan9.Qid{Type: QTFile, Path: qidEvents}, Mode: 0644, Name: "events",
Uid: "q", Gid: "q", Muid: "q",
@ -1047,39 +1036,6 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte {
return data[offset:end]
}
func (s *Server) readAuditLog(offset uint64, count uint32) []byte {
mailMgr := s.mgr.GetMailManager()
if mailMgr == nil {
return nil
}
auditLog := mailMgr.GetAuditLog()
if auditLog == nil {
return nil
}
// Try to read from current offset
logData, hasMore := auditLog.ReadFrom(int64(offset))
if hasMore && len(logData) > 0 {
// Return available data
end := min(int(count), len(logData))
return []byte(logData[:end])
}
// No data available at this offset, wait for new data
waitCh := auditLog.WaitForData()
<-waitCh
// Check again after new data arrived
logData, hasMore = auditLog.ReadFrom(int64(offset))
if hasMore && len(logData) > 0 {
end := min(int(count), len(logData))
return []byte(logData[:end])
}
return nil
}
func (s *Server) readFile(path string) string {
// Handle beads paths
if strings.HasPrefix(path, "/beads/") {
@ -1117,18 +1073,6 @@ func (s *Server) readFile(path string) string {
return string(s.events.Read())
}
if path == "/audit" {
mailMgr := s.mgr.GetMailManager()
if mailMgr == nil {
return ""
}
auditLog := mailMgr.GetAuditLog()
if auditLog == nil {
return ""
}
return auditLog.Read()
}
if path == "/status" {
var lines []string
mailMgr := s.mgr.GetMailManager()