The PR #147 review forged origin: static as a JWT principal through three tools the first version never touched (brain_edit, brain_ingest_raw, ingest_raw + brain_promote), and through YAML spellings a key-name match missed (quoted, escaped, tagged, flow-mapped, merge keys, lone CR). - api.StampProvenance removes every spelling of origin/written_by from the frontmatter and appends the real values. A line is dropped if EITHER yaml.v3 (decoding quotes, escapes, tags, flow maps) or the brain's line-oriented readers would read it as provenance; complex keys, orphan values and merge keys are dropped outright. Lone CRs in frontmatter become LF. Principal values that are not bare words are quoted. - Applied as the last step of writeHallNote, UpdateNote, PromoteNote (re-attributed to the promoter), brain_edit (re-attributed to the editor) and both ingests (every page written or merged). REST /write and /promote carry no principal and stamp unknown. - Ingest: newlines in scalar fields fold to spaces; a page type must be a plain lower-case name (../raw wrote outside wiki/, pre-existing). - Audit: outcome is ok|error, never the error text, which echoed argument values. Tests: every escape from the review is a table case asserting the invariant on the file (parses as YAML, yaml.v3 and brain_get agree, exactly the stamped lines). Plus REST /write, REST capture AuthPath, body-line provenance in search, and quoted principals; two of the review's surviving flips now go red. Three tests renamed so names claim only what they assert. Refs #145 #144 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
49 lines
1.4 KiB
Go
49 lines
1.4 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))
|
|
}
|
|
}
|
|
// 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...)
|
|
}
|