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)) } } outcome := "ok" if err != nil { outcome = err.Error() if len(outcome) > 200 { outcome = outcome[:200] } } attrs = append(attrs, "outcome", outcome) slog.InfoContext(ctx, "mcp tool call", attrs...) }