feat(brain): audit-log every tools/call; server-owned write provenance (#147)
Closes #144 (audit line per tools/call) and #145 (origin/written_by stamped on every model-reachable write, surfaced in brain_query/brain_get). Passed pr-gatekeeper after four rounds of adversarial review, verified by execution: #147 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Mathias <mthbqv@gmail.com>
This commit was merged in pull request #147.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
package mcp_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// captureToolCallLog routes slog to a buffer for fn and returns the decoded
|
||||
// "mcp tool call" records.
|
||||
func captureToolCallLog(t *testing.T, fn func()) []map[string]any {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
prev := slog.Default()
|
||||
slog.SetDefault(slog.New(slog.NewJSONHandler(&buf, nil)))
|
||||
t.Cleanup(func() { slog.SetDefault(prev) })
|
||||
fn()
|
||||
var out []map[string]any
|
||||
for _, l := range strings.Split(strings.TrimSpace(buf.String()), "\n") {
|
||||
var rec map[string]any
|
||||
if json.Unmarshal([]byte(l), &rec) == nil && rec["msg"] == "mcp tool call" {
|
||||
out = append(out, rec)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// hyperguild#144: brain_query/brain_read left no record of who read what.
|
||||
func TestToolsCallLogsOneRecordWithToolPrincipalOriginAndScope(t *testing.T) {
|
||||
srv, _, _, _ := govServer(t)
|
||||
recs := captureToolCallLog(t, func() {
|
||||
authCall(t, srv, "Bearer "+capStaticTok, "brain_query",
|
||||
map[string]any{"query": "anything", "wing": "homelab", "hall": "facts"})
|
||||
})
|
||||
require.Len(t, recs, 1)
|
||||
r := recs[0]
|
||||
assert.Equal(t, "brain_query", r["tool"])
|
||||
assert.Equal(t, "local-cli", r["principal"])
|
||||
assert.Equal(t, "static", r["origin"])
|
||||
assert.Equal(t, "homelab", r["wing"])
|
||||
assert.Equal(t, "facts", r["hall"])
|
||||
assert.Equal(t, "ok", r["outcome"])
|
||||
assert.Contains(t, r, "duration_ms")
|
||||
}
|
||||
|
||||
func TestToolsCallWithoutDerivablePrincipalLogsOriginUnknown(t *testing.T) {
|
||||
srv, _, _, _ := govServer(t)
|
||||
recs := captureToolCallLog(t, func() {
|
||||
authCall(t, srv, "", "brain_query", map[string]any{"query": "x"})
|
||||
})
|
||||
require.Len(t, recs, 1)
|
||||
assert.Equal(t, "unknown", recs[0]["origin"])
|
||||
assert.Equal(t, "", recs[0]["principal"])
|
||||
}
|
||||
|
||||
func TestToolsCallLogsErrorOutcome(t *testing.T) {
|
||||
srv, _, _, _ := govServer(t)
|
||||
recs := captureToolCallLog(t, func() {
|
||||
authCall(t, srv, "Bearer "+capStaticTok, "brain_query", map[string]any{})
|
||||
})
|
||||
require.Len(t, recs, 1)
|
||||
assert.Equal(t, "error", recs[0]["outcome"])
|
||||
}
|
||||
|
||||
// The audit line must not become a second copy of brain content.
|
||||
func TestToolsCallLogOmitsContentButRecordsItsSize(t *testing.T) {
|
||||
const sentinel = "SENTINEL-BRAIN-CONTENT-91c2"
|
||||
srv, _, _, _ := govServer(t)
|
||||
recs := captureToolCallLog(t, func() {
|
||||
authCall(t, srv, "Bearer "+capStaticTok, "brain_write", map[string]any{
|
||||
"content": "# t\n\n" + sentinel, "wing": "testing", "hall": "facts", "filename": "audit-sentinel",
|
||||
})
|
||||
})
|
||||
require.Len(t, recs, 1)
|
||||
raw, _ := json.Marshal(recs[0])
|
||||
assert.NotContains(t, string(raw), sentinel)
|
||||
assert.EqualValues(t, len("# t\n\n"+sentinel), recs[0]["content_bytes"])
|
||||
}
|
||||
Reference in New Issue
Block a user