feat(brain): stamp server-owned write provenance; surface it in brain_query
CI / Lint / Test / Vet (pull_request) Successful in 43s
CI / Mirror to GitHub (pull_request) Skipped

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>
This commit is contained in:
2026-10-01 20:49:11 +02:00
co-authored by Claude Opus 5.5
parent a9a9eebe26
commit 32dedb318d
13 changed files with 277 additions and 25 deletions
+12
View File
@@ -95,6 +95,18 @@ func (f *frontmatter) set(key, value string) {
f.lines = append(f.lines, fmLine{key: key, value: value}) f.lines = append(f.lines, fmLine{key: key, value: value})
} }
// del removes every top-level line for key. Continuation lines of a
// multi-line value are not tracked per key, so del is only for scalars.
func (f *frontmatter) del(key string) {
kept := f.lines[:0]
for _, l := range f.lines {
if l.key != key {
kept = append(kept, l)
}
}
f.lines = kept
}
// render serialises the frontmatter back into a `---`-fenced block. An // render serialises the frontmatter back into a `---`-fenced block. An
// empty frontmatter renders to the empty string so bodies without a // empty frontmatter renders to the empty string so bodies without a
// header stay header-less. // header stay header-less.
+29 -1
View File
@@ -118,6 +118,20 @@ type WriteNoteOptions struct {
Wing string Wing string
Hall string Hall string
SourceType string // "internal" marks a first-party observation (e.g. claudewatcher) that needs no external citation SourceType string // "internal" marks a first-party observation (e.g. claudewatcher) that needs no external citation
// Origin/WrittenBy are server-owned provenance (hyperguild#145): any
// origin/written_by in Content's own frontmatter is discarded, so a
// caller cannot claim a more trusted origin than it authenticated as.
Origin string
WrittenBy string
}
// provenanceOrigin normalises an origin for the frontmatter: absence must
// never read as trusted, so empty becomes "unknown".
func provenanceOrigin(o string) string {
if o == "" {
return "unknown"
}
return o
} }
// WriteNote writes a markdown note into the brain. Returns the path // WriteNote writes a markdown note into the brain. Returns the path
@@ -180,6 +194,11 @@ func writeHallNote(brainDir string, opts WriteNoteOptions) (string, error) {
fmt.Fprintf(&fm, "hall: %s\n", opts.Hall) fmt.Fprintf(&fm, "hall: %s\n", opts.Hall)
fmt.Fprintf(&fm, "created_at: %s\n", time.Now().UTC().Format(time.RFC3339)) fmt.Fprintf(&fm, "created_at: %s\n", time.Now().UTC().Format(time.RFC3339))
emitted["wing"], emitted["hall"], emitted["created_at"] = true, true, true emitted["wing"], emitted["hall"], emitted["created_at"] = true, true, true
fmt.Fprintf(&fm, "origin: %s\n", provenanceOrigin(opts.Origin))
emitted["origin"], emitted["written_by"] = true, true
if opts.WrittenBy != "" {
fmt.Fprintf(&fm, "written_by: %s\n", opts.WrittenBy)
}
// writeField merges one key: opts.Content's own value (if the note already // writeField merges one key: opts.Content's own value (if the note already
// carries this field in its own frontmatter) always wins over the fallback, // carries this field in its own frontmatter) always wins over the fallback,
@@ -295,7 +314,16 @@ func (h *Handler) Write(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "invalid JSON") writeError(w, http.StatusBadRequest, "invalid JSON")
return return
} }
relPath, err := WriteNote(h.brainDir, WriteNoteOptions(req)) // Field by field, not a struct conversion: provenance is server-owned and
// this door carries no principal, so it stamps origin: unknown
// (hyperguild#145). A JSON body must never be able to set it.
relPath, err := WriteNote(h.brainDir, WriteNoteOptions{
Content: req.Content,
Filename: req.Filename,
Wing: req.Wing,
Hall: req.Hall,
SourceType: req.SourceType,
})
if err != nil { if err != nil {
h.logger.Error("write failed", "err", err) h.logger.Error("write failed", "err", err)
writeError(w, http.StatusBadRequest, err.Error()) writeError(w, http.StatusBadRequest, err.Error())
+13 -1
View File
@@ -46,6 +46,11 @@ type UpdateNoteOptions struct {
Slug string Slug string
Content string // new full body (whole-note replace) Content string // new full body (whole-note replace)
Reason string // optional; stamped as supersede_reason Reason string // optional; stamped as supersede_reason
// Origin/WrittenBy re-stamp provenance to the updating principal: the
// body is replaced wholesale, so the last writer is the author of what
// a reader sees (hyperguild#145). Never taken from Content's fragment.
Origin string
WrittenBy string
} }
// UpdateNote supersedes an existing note in place. It replaces the body // UpdateNote supersedes an existing note in place. It replaces the body
@@ -122,12 +127,19 @@ func UpdateNote(brainDir string, opts UpdateNoteOptions) (relPath, contentHash,
// overwrites below to save the other two keeps the invariant // overwrites below to save the other two keeps the invariant
// explicit at the one place it can be silently broken by a future // explicit at the one place it can be silently broken by a future
// reordering, not implicit in call order. // reordering, not implicit in call order.
if l.key == "" || l.key == "updated_at" || l.key == "supersedes" || l.key == "supersede_reason" { if l.key == "" || l.key == "updated_at" || l.key == "supersedes" || l.key == "supersede_reason" ||
l.key == "origin" || l.key == "written_by" {
continue continue
} }
fm.set(l.key, l.value) fm.set(l.key, l.value)
} }
fm.set("updated_at", time.Now().UTC().Format(time.RFC3339)) fm.set("updated_at", time.Now().UTC().Format(time.RFC3339))
fm.set("origin", provenanceOrigin(opts.Origin))
if opts.WrittenBy != "" {
fm.set("written_by", opts.WrittenBy)
} else {
fm.del("written_by")
}
fm.set("supersedes", priorHash) fm.set("supersedes", priorHash)
if opts.Reason != "" { if opts.Reason != "" {
fm.set("supersede_reason", opts.Reason) fm.set("supersede_reason", opts.Reason)
+3 -1
View File
@@ -53,6 +53,8 @@ func (s *Store) Write(ctx context.Context, n capture.Note) (capture.Ref, error)
Filename: n.Filename, Filename: n.Filename,
Wing: n.Wing, Wing: n.Wing,
Hall: n.Hall, Hall: n.Hall,
Origin: n.Origin,
WrittenBy: n.WrittenBy,
}) })
if err != nil { if err != nil {
return capture.Ref{}, err return capture.Ref{}, err
@@ -74,7 +76,7 @@ func (s *Store) Update(ctx context.Context, slug string, n capture.Note) (captur
} }
n.Content = content n.Content = content
opts := api.UpdateNoteOptions{Content: n.Content, Reason: n.Reason} opts := api.UpdateNoteOptions{Content: n.Content, Reason: n.Reason, Origin: n.Origin, WrittenBy: n.WrittenBy}
if strings.Contains(slug, "/") { if strings.Contains(slug, "/") {
opts.Path = slug opts.Path = slug
} else { } else {
+1
View File
@@ -60,6 +60,7 @@ type CaptureContext struct {
Classification string // caller-declared level token ("" = unspecified) Classification string // caller-declared level token ("" = unspecified)
Principal string // server-derived (auth); audit identity Principal string // server-derived (auth); audit identity
Origin Zone // server-derived trust zone; the I1 gate input Origin Zone // server-derived trust zone; the I1 gate input
AuthPath string // server-derived: "static" or "jwt"; stamped as note provenance (#145)
} }
// Insight is one piece of session knowledge bound for the brain. A // Insight is one piece of session knowledge bound for the brain. A
+5
View File
@@ -40,6 +40,11 @@ type Note struct {
Wing string Wing string
Hall string Hall string
Reason string Reason string
// Provenance (hyperguild#145) is stamped by the server from the
// authenticated principal, never taken from Content's frontmatter.
// Origin is "static", "jwt" or "unknown"; empty means "unknown".
Origin string
WrittenBy string
} }
// BrainStore is the brain persistence port — the shared implementation of // BrainStore is the brain persistence port — the shared implementation of
+6 -3
View File
@@ -142,7 +142,7 @@ func (s *Service) Capture(ctx context.Context, in CaptureInput) (CaptureReceipt,
var landed []string var landed []string
for i, ins := range in.Insights { for i, ins := range in.Insights {
res, item, err := s.persistInsight(ctx, ins) res, item, err := s.persistInsight(ctx, ins, in.Context)
receipt.Insights = append(receipt.Insights, res) receipt.Insights = append(receipt.Insights, res)
if err != nil { if err != nil {
receipt.Errors = append(receipt.Errors, ItemError{Item: fmt.Sprintf("insight[%d]", i), Error: err.Error()}) receipt.Errors = append(receipt.Errors, ItemError{Item: fmt.Sprintf("insight[%d]", i), Error: err.Error()})
@@ -278,8 +278,11 @@ func escalationReason(escalators []string) string {
return "required by: " + strings.Join(escalators, ", ") return "required by: " + strings.Join(escalators, ", ")
} }
func (s *Service) persistInsight(ctx context.Context, ins Insight) (InsightResult, string, error) { func (s *Service) persistInsight(ctx context.Context, ins Insight, cc CaptureContext) (InsightResult, string, error) {
note := Note{Content: ins.Text, Wing: ins.Wing, Hall: ins.Hall, Filename: brain.Sanitise(firstLine(ins.Text))} note := Note{
Content: ins.Text, Wing: ins.Wing, Hall: ins.Hall, Filename: brain.Sanitise(firstLine(ins.Text)),
Origin: cc.AuthPath, WrittenBy: cc.Principal,
}
var ref Ref var ref Ref
var err error var err error
if ins.SupersedeSlug != "" { if ins.SupersedeSlug != "" {
@@ -104,6 +104,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// caller may have tried to put in the body. // caller may have tried to put in the body.
in.Context.Principal = principal in.Context.Principal = principal
in.Context.Origin = h.resolver.Resolve(principal, viaStatic) in.Context.Origin = h.resolver.Resolve(principal, viaStatic)
in.Context.AuthPath = "jwt"
if viaStatic {
in.Context.AuthPath = "static"
}
rec, err := h.svc.Capture(r.Context(), in) rec, err := h.svc.Capture(r.Context(), in)
switch { switch {
+1 -7
View File
@@ -12,13 +12,7 @@ import (
// ended. Brain content is never copied into the record — only its size — so // 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. // 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) { func logToolCall(ctx context.Context, name string, args json.RawMessage, start time.Time, err error) {
principal, origin := "", "unknown" origin, principal := provenanceFrom(ctx)
if info, ok := ctx.Value(principalKey).(principalInfo); ok {
principal, origin = info.principal, "jwt"
if info.viaStatic {
origin = "static"
}
}
attrs := []any{ attrs := []any{
"tool", name, "tool", name,
"principal", principal, "principal", principal,
+6
View File
@@ -255,11 +255,14 @@ func (s *Server) brainWrite(ctx context.Context, args json.RawMessage) (json.Raw
// one implementation (capture uses the same store). The read-after-write // one implementation (capture uses the same store). The read-after-write
// handle {id, path, content_hash} comes back from the store; path is kept // handle {id, path, content_hash} comes back from the store; path is kept
// for backward compatibility. // for backward compatibility.
origin, writtenBy := provenanceFrom(ctx)
ref, err := s.store.Write(ctx, capture.Note{ ref, err := s.store.Write(ctx, capture.Note{
Content: a.Content, Content: a.Content,
Filename: a.Filename, Filename: a.Filename,
Wing: a.Wing, Wing: a.Wing,
Hall: a.Hall, Hall: a.Hall,
Origin: origin,
WrittenBy: writtenBy,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@@ -304,11 +307,14 @@ func (s *Server) brainUpdate(ctx context.Context, args json.RawMessage) (json.Ra
if a.Path != "" { if a.Path != "" {
slug = a.Path slug = a.Path
} }
origin, writtenBy := provenanceFrom(ctx)
ref, err := s.store.Update(ctx, slug, capture.Note{ ref, err := s.store.Update(ctx, slug, capture.Note{
Content: a.Content, Content: a.Content,
Wing: a.Wing, Wing: a.Wing,
Hall: a.Hall, Hall: a.Hall,
Reason: a.Reason, Reason: a.Reason,
Origin: origin,
WrittenBy: writtenBy,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
+124
View File
@@ -0,0 +1,124 @@
package mcp_test
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// resultText decodes the tool result payload of a tools/call response.
func resultText(t *testing.T, resp map[string]any) map[string]any {
t.Helper()
require.NotContains(t, resp, "error", "tool call failed: %v", resp["error"])
content := resp["result"].(map[string]any)["content"].([]any)
var out map[string]any
require.NoError(t, json.Unmarshal([]byte(content[0].(map[string]any)["text"].(string)), &out))
return out
}
func readNote(t *testing.T, dir, rel string) string {
t.Helper()
b, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(rel)))
require.NoError(t, err)
return string(b)
}
func frontmatterOf(note string) string {
parts := strings.SplitN(note, "---\n", 3)
if len(parts) < 3 {
return ""
}
return parts[1]
}
// hyperguild#145: a brain entry records which principal wrote it, so a reader
// can tell a local-CLI note from one a connector session wrote after reading
// an untrusted page.
func TestBrainWriteStampsPrincipalProvenance(t *testing.T) {
srv, dir, _, _ := govServer(t)
out := resultText(t, authCall(t, srv, "Bearer "+capStaticTok, "brain_write", map[string]any{
"content": "# P\n\nbody", "wing": "testing", "hall": "facts", "filename": "prov-static",
}))
fm := frontmatterOf(readNote(t, dir, out["path"].(string)))
assert.Contains(t, fm, "origin: static\n")
assert.Contains(t, fm, "written_by: local-cli\n")
}
func TestBrainWriteWithoutPrincipalStampsOriginUnknown(t *testing.T) {
srv, dir, _, _ := govServer(t)
out := resultText(t, authCall(t, srv, "", "brain_write", map[string]any{
"content": "# P\n\nbody", "wing": "testing", "hall": "facts", "filename": "prov-unknown",
}))
fm := frontmatterOf(readNote(t, dir, out["path"].(string)))
assert.Contains(t, fm, "origin: unknown\n")
assert.NotContains(t, fm, "written_by:")
}
// The control: a caller cannot claim a more trusted origin by putting it in
// its own frontmatter.
func TestBrainWriteOverwritesCallerSuppliedProvenance(t *testing.T) {
srv, dir, _, _ := govServer(t)
forged := "---\norigin: static\nwritten_by: local-cli\n---\n# F\n\nbody"
out := resultText(t, authCall(t, srv, "", "brain_write", map[string]any{
"content": forged, "wing": "testing", "hall": "facts", "filename": "prov-forged",
}))
fm := frontmatterOf(readNote(t, dir, out["path"].(string)))
assert.Contains(t, fm, "origin: unknown\n")
assert.NotContains(t, fm, "origin: static")
assert.NotContains(t, fm, "written_by: local-cli")
assert.Equal(t, 1, strings.Count(fm, "origin:"), "exactly one origin line")
}
// brain_update replaces the content wholesale, so provenance follows the
// last writer — and a forged fragment must not survive the merge either.
func TestBrainUpdateRestampsProvenanceAndIgnoresForgedFragment(t *testing.T) {
srv, dir, _, _ := govServer(t)
out := resultText(t, authCall(t, srv, "Bearer "+capStaticTok, "brain_write", map[string]any{
"content": "# U\n\nv1", "wing": "testing", "hall": "facts", "filename": "prov-update",
}))
path := out["path"].(string)
resultText(t, authCall(t, srv, "", "brain_update", map[string]any{
"path": path, "content": "---\norigin: static\nwritten_by: local-cli\n---\n# U\n\nv2",
}))
fm := frontmatterOf(readNote(t, dir, path))
assert.Contains(t, fm, "origin: unknown\n")
assert.NotContains(t, fm, "written_by: local-cli")
assert.Equal(t, 1, strings.Count(fm, "origin:"))
}
func TestBrainQueryReturnsProvenance(t *testing.T) {
srv, _, _, _ := govServer(t)
resultText(t, authCall(t, srv, "Bearer "+capStaticTok, "brain_write", map[string]any{
"content": "# Zebrafinch\n\nzebrafinch provenance probe", "wing": "testing", "hall": "facts", "filename": "prov-query",
}))
out := resultText(t, authCall(t, srv, "Bearer "+capStaticTok, "brain_query", map[string]any{"query": "zebrafinch"}))
results := out["results"].([]any)
require.NotEmpty(t, results)
hit := results[0].(map[string]any)
assert.Equal(t, "static", hit["origin"])
assert.Equal(t, "local-cli", hit["written_by"])
}
// capture is the door a claude.ai connector session uses, so it is the one
// whose provenance matters most: a JWT principal must be stamped as such.
func TestCaptureStampsJWTProvenance(t *testing.T) {
srv, brainDir := captureServer(t, capFakeValidator{subject: "koala-cli"}, []string{"koala-cli"})
resp := captureCall(t, srv, "Bearer jwt-token", map[string]any{
"context": map[string]any{"harness": "claude-code", "actor": "mathias", "classification": "internal"},
"insights": []map[string]any{{"text": "Capture provenance probe", "wing": "hyperguild", "hall": "facts"}},
})
require.Nil(t, resp["error"], "got error: %v", resp["error"])
matches, err := filepath.Glob(filepath.Join(brainDir, "wiki/hyperguild/facts/*.md"))
require.NoError(t, err)
require.Len(t, matches, 1)
b, err := os.ReadFile(matches[0])
require.NoError(t, err)
fm := frontmatterOf(string(b))
assert.Contains(t, fm, "origin: jwt\n")
assert.Contains(t, fm, "written_by: koala-cli\n")
}
+15
View File
@@ -90,6 +90,7 @@ func (s *Server) brainCapture(ctx context.Context, args json.RawMessage) (json.R
// Principal and origin are server-derived — never taken from the body. // Principal and origin are server-derived — never taken from the body.
in.Context.Principal = info.principal in.Context.Principal = info.principal
in.Context.Origin = s.capture.resolver.Resolve(info.principal, info.viaStatic) in.Context.Origin = s.capture.resolver.Resolve(info.principal, info.viaStatic)
in.Context.AuthPath, _ = provenanceFrom(ctx)
rec, err := s.capture.svc.Capture(ctx, in) rec, err := s.capture.svc.Capture(ctx, in)
if err != nil { if err != nil {
@@ -99,3 +100,17 @@ func (s *Server) brainCapture(ctx context.Context, args json.RawMessage) (json.R
} }
return json.Marshal(rec) return json.Marshal(rec)
} }
// provenanceFrom maps the authenticated principal in ctx to the provenance
// stamped on a brain write (hyperguild#145). No principal → "unknown", never
// silence: an unstamped note must not read as trusted.
func provenanceFrom(ctx context.Context) (origin, writtenBy string) {
info, ok := ctx.Value(principalKey).(principalInfo)
if !ok {
return "unknown", ""
}
if info.viaStatic {
return "static", info.principal
}
return "jwt", info.principal
}
+46
View File
@@ -48,6 +48,11 @@ type Result struct {
// (infra#72). Read from frontmatter when present, otherwise // (infra#72). Read from frontmatter when present, otherwise
// inferred from the parent directory. // inferred from the parent directory.
Tier string `json:"tier,omitempty"` Tier string `json:"tier,omitempty"`
// Origin/WrittenBy surface write provenance (hyperguild#145) so a
// reader can discount an entry a connector session wrote. Omitted for
// notes written before provenance existed — never fabricated.
Origin string `json:"origin,omitempty"`
WrittenBy string `json:"written_by,omitempty"`
} }
// tierWeight maps the DIKW tier to a score multiplier applied right // tierWeight maps the DIKW tier to a score multiplier applied right
@@ -146,6 +151,7 @@ func QueryContext(ctx context.Context, brainDir string, opts QueryOptions) ([]Re
rel = filepath.ToSlash(rel) rel = filepath.ToSlash(rel)
wing, hall := extractWingHall(string(content), rel) wing, hall := extractWingHall(string(content), rel)
tier := extractTier(string(content), rel) tier := extractTier(string(content), rel)
origin, writtenBy := extractProvenance(string(content))
results = append(results, Result{ results = append(results, Result{
Path: rel, Path: rel,
Title: extractTitle(string(content), d.Name()), Title: extractTitle(string(content), d.Name()),
@@ -154,6 +160,9 @@ func QueryContext(ctx context.Context, brainDir string, opts QueryOptions) ([]Re
Wing: wing, Wing: wing,
Hall: hall, Hall: hall,
Tier: tier, Tier: tier,
Origin: origin,
WrittenBy: writtenBy,
}) })
return nil return nil
}) })
@@ -272,6 +281,7 @@ func hydrate(brainDir, relPath string) (Result, error) {
} }
wing, hall := extractWingHall(string(content), relPath) wing, hall := extractWingHall(string(content), relPath)
tier := extractTier(string(content), relPath) tier := extractTier(string(content), relPath)
origin, writtenBy := extractProvenance(string(content))
return Result{ return Result{
Path: relPath, Path: relPath,
Title: extractTitle(string(content), filepath.Base(relPath)), Title: extractTitle(string(content), filepath.Base(relPath)),
@@ -279,9 +289,45 @@ func hydrate(brainDir, relPath string) (Result, error) {
Wing: wing, Wing: wing,
Hall: hall, Hall: hall,
Tier: tier, Tier: tier,
Origin: origin,
WrittenBy: writtenBy,
}, nil }, nil
} }
// extractProvenance reads origin/written_by from the leading frontmatter
// block only — a body line that happens to start with "origin:" is not
// provenance.
func extractProvenance(content string) (origin, writtenBy string) {
scanner := bufio.NewScanner(strings.NewReader(content))
inFrontmatter := false
for scanner.Scan() {
line := scanner.Text()
if strings.TrimSpace(line) == "---" {
if !inFrontmatter {
inFrontmatter = true
continue
}
break
}
if !inFrontmatter {
return "", ""
}
key, val, ok := strings.Cut(line, ":")
if !ok || strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") {
continue
}
v := strings.Trim(strings.TrimSpace(val), `"'`)
switch strings.TrimSpace(key) {
case "origin":
origin = v
case "written_by":
writtenBy = v
}
}
return origin, writtenBy
}
// resolveRoots returns the directories to walk for the given wing/hall // resolveRoots returns the directories to walk for the given wing/hall
// filters. Validates hall against the closed vocabulary when set. // filters. Validates hall against the closed vocabulary when set.
func resolveRoots(brainDir, wing, hall string) ([]string, error) { func resolveRoots(brainDir, wing, hall string) ([]string, error) {