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:
parent
d5eac1d2c1
commit
c6987a7962
6
BEADS.md
6
BEADS.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Reference in New Issue