brain_write was an ungated, durable prompt-injection path: a session that just read an untrusted page could write an entry every later session retrieves as trusted guidance (OWASP MCP06). Decision (#145): tag provenance rather than stage writes. Every write and update stamps origin (static | jwt | unknown) and written_by (the authenticated principal) into frontmatter. Server-owned: origin/written_by in caller content or an update fragment are discarded, so a caller cannot claim a more trusted origin. No principal stamps origin: unknown, never silence. brain_update re-stamps to the last writer. capture (the claude.ai connector door) stamps too, via a new server-derived CaptureContext.AuthPath set by both capture doors after decode. REST POST /write carries no principal and stamps unknown. brain_query results carry origin/written_by when the note has them; older notes omit them rather than fabricate. brain_get already returns frontmatter. Refs #145 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
48 lines
1.2 KiB
Go
48 lines
1.2 KiB
Go
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...)
|
|
}
|