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,48 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"time"
|
||||
)
|
||||
|
||||
// logToolCall emits one structured record per tools/call (hyperguild#144,
|
||||
// OWASP MCP08): who called which tool, against which wing/hall, and how it
|
||||
// ended. Brain content is never copied into the record — only its size — so
|
||||
// the audit trail does not become a second, unscrubbed copy of the brain.
|
||||
func logToolCall(ctx context.Context, name string, args json.RawMessage, start time.Time, err error) {
|
||||
origin, principal := provenanceFrom(ctx)
|
||||
attrs := []any{
|
||||
"tool", name,
|
||||
"principal", principal,
|
||||
"origin", origin,
|
||||
"duration_ms", time.Since(start).Milliseconds(),
|
||||
}
|
||||
var a struct {
|
||||
Wing string `json:"wing"`
|
||||
Hall string `json:"hall"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
if json.Unmarshal(args, &a) == nil {
|
||||
if a.Wing != "" {
|
||||
attrs = append(attrs, "wing", a.Wing)
|
||||
}
|
||||
if a.Hall != "" {
|
||||
attrs = append(attrs, "hall", a.Hall)
|
||||
}
|
||||
if a.Content != "" {
|
||||
attrs = append(attrs, "content_bytes", len(a.Content))
|
||||
}
|
||||
}
|
||||
// The outcome is a class, never the error text: handler errors echo
|
||||
// argument values (a rejected half_life, a requested path), which would
|
||||
// make the audit line a copy of what the caller sent (PR #147 review E6).
|
||||
// The caller already received the full error in the tool response.
|
||||
outcome := "ok"
|
||||
if err != nil {
|
||||
outcome = "error"
|
||||
}
|
||||
attrs = append(attrs, "outcome", outcome)
|
||||
slog.InfoContext(ctx, "mcp tool call", attrs...)
|
||||
}
|
||||
Reference in New Issue
Block a user