feat(mcp): record and alert on a governance-file rewrite (#97) (#98)
CI / Lint / Test / Vet (push) Successful in 1m23s
CI / Mirror to GitHub (push) Successful in 4s

brain_edit_root was gated but unobserved: on success it emitted a slog.Warn
and nothing else. That proves a rewrite was allowed, not that one happened —
and brain-sync commits it within 15 minutes authored `brain-sync`, so git
history cannot tell a CLAUDE.md rewrite apart from routine note traffic.

A successful brain_edit_root now emits an ntfy notification and an AuditEntry
through the same sink a capture already uses. notes.Result gains
PrevContentHash so both ends of the change are identifiable.

Path and hashes only in the notification body. Reserved at Internal, not
Confidential, because Confidential makes DegradingSink refuse to record while
loki is down rather than buffer. Observation is best-effort and never fails
the call; a failure to observe logs at ERROR.

Ordinary brain_edit on knowledge//wiki/ is deliberately not audited — git
history already carries the same before/after bytes with a diff.

Closes #97.

Co-authored-by: Mathias <mthbqv@gmail.com>
This commit was merged in pull request #98.
This commit is contained in:
2026-08-22 06:09:53 +00:00
committed by mathias
parent 055af368cd
commit 36a45068ab
5 changed files with 291 additions and 25 deletions
+15 -9
View File
@@ -36,11 +36,15 @@ var pathLocks sync.Map // string -> *sync.Mutex
// Result is what a successful edit reports.
type Result struct {
Path string `json:"path"`
ContentHash string `json:"content_hash"`
BytesBefore int `json:"bytes_before"`
BytesAfter int `json:"bytes_after"`
ReindexError string `json:"reindex_error,omitempty"`
Path string `json:"path"`
// PrevContentHash is the sha256 of the file as it was before this edit.
// Paired with ContentHash it identifies a rewrite in an audit trail or an
// alert without carrying any of the bytes (hyperguild#97).
PrevContentHash string `json:"prev_content_hash"`
ContentHash string `json:"content_hash"`
BytesBefore int `json:"bytes_before"`
BytesAfter int `json:"bytes_after"`
ReindexError string `json:"reindex_error,omitempty"`
}
// Read returns a note's bytes verbatim.
@@ -149,11 +153,13 @@ func Edit(brainDir, path, oldStr, newStr string, scope Scope) (Result, error) {
}
sum := sha256.Sum256([]byte(after))
prev := sha256.Sum256(before)
return Result{
Path: t.Rel,
ContentHash: hex.EncodeToString(sum[:]),
BytesBefore: len(before),
BytesAfter: len(after),
Path: t.Rel,
PrevContentHash: hex.EncodeToString(prev[:]),
ContentHash: hex.EncodeToString(sum[:]),
BytesBefore: len(before),
BytesAfter: len(after),
}, nil
}