Round 2 of the PR #147 review forged origin: static for yaml.v3 with an
alias key plus a "..." document-end line: per-line judgement cannot see
context-dependent YAML. It also found REST ingest writing pages with no
provenance (stampPages lived only in the MCP server).
- New package internal/provenance. Apply keeps only top-level groups
`key: value` with a bare-identifier key whose group decodes on its own
to exactly that key, drops reserved and duplicate keys and everything
else (aliases, anchors as keys, document markers, complex/merge keys,
flow maps, tags, quoted keys), appends the stamp, then decodes the
whole result with yaml.v3 and refuses the write unless it reads back
exactly the stamp. api.StampProvenance wraps it and now returns an
error, which every caller propagates.
- The ingest pipeline stamps every page it writes or merges, from
provenance.FromContext(ctx); MCP attaches the caller's stamp, REST and
the watcher stamp unknown. RunRaw takes a ctx.
- brain_edit snapshots the note and rolls the edit back if the stamp
cannot be verified.
Tests: R1/R2 payloads join the write/update variant table, plus
brain_edit with the alias payload; REST /ingest-raw (title newline
checked with the line-oriented reader) and REST /ingest (stub LLM) stamp
unknown. Two existing byte-exact tests updated for the stamp line.
Refs #145#144
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
Closes#125. Skips updated_at/supersedes/supersede_reason in the caller fragment merge loop so they cannot be set via the fragment, only via the intended entry points.
delete_branch_after_merge=true
Co-authored-by: Mathias <mthbqv@gmail.com>
The BrainStore is not the only door. POST /write — the HTTP path CLAUDE.md
hands to curl and scripts — calls api.WriteNote directly, so the strip
shipped one commit ago covered brain_write and capture while leaving the raw
door ungated. A gate one door wide is not a gate.
The BrainStore keeps its own call so it can still report Ref.Unlinked to the
MCP caller; running the strip twice is a no-op on content the primitive has
already cleaned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Implements the api-layer half of #45. UpdateNote supersedes a note in
place (whole-note body replace, frontmatter re-stamp: updated_at,
supersedes=prior content hash, supersede_reason), preserving created_at,
wing, hall, and any custom fields. Never creates — a missing target is
an error so callers fall back to brain_write. ReadNote is the read-after-
write primitive (frontmatter + body + content_hash). ContentHash is the
sha256 handle that round-trips write/update → get.
Frontmatter is edited via a line-preserving ordered editor rather than a
yaml.v3 round-trip, which would reorder keys and strip comments — the
brain writes flat key:value frontmatter by hand.
Embeddings are not refreshed here: the rewritten file's mtime advances,
which the out-of-band vectorstore.Sync ticker uses to re-embed it — the
same mechanism brain_write relies on.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>