feat(brain): stamp server-owned write provenance; surface it in brain_query
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:
@@ -118,6 +118,20 @@ type WriteNoteOptions struct {
|
||||
Wing string
|
||||
Hall string
|
||||
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
|
||||
@@ -180,6 +194,11 @@ func writeHallNote(brainDir string, opts WriteNoteOptions) (string, error) {
|
||||
fmt.Fprintf(&fm, "hall: %s\n", opts.Hall)
|
||||
fmt.Fprintf(&fm, "created_at: %s\n", time.Now().UTC().Format(time.RFC3339))
|
||||
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
|
||||
// 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")
|
||||
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 {
|
||||
h.logger.Error("write failed", "err", err)
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
|
||||
Reference in New Issue
Block a user