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:
@@ -60,6 +60,7 @@ type CaptureContext struct {
|
||||
Classification string // caller-declared level token ("" = unspecified)
|
||||
Principal string // server-derived (auth); audit identity
|
||||
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
|
||||
|
||||
@@ -40,6 +40,11 @@ type Note struct {
|
||||
Wing string
|
||||
Hall 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
|
||||
|
||||
@@ -142,7 +142,7 @@ func (s *Service) Capture(ctx context.Context, in CaptureInput) (CaptureReceipt,
|
||||
var landed []string
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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, ", ")
|
||||
}
|
||||
|
||||
func (s *Service) persistInsight(ctx context.Context, ins Insight) (InsightResult, string, error) {
|
||||
note := Note{Content: ins.Text, Wing: ins.Wing, Hall: ins.Hall, Filename: brain.Sanitise(firstLine(ins.Text))}
|
||||
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)),
|
||||
Origin: cc.AuthPath, WrittenBy: cc.Principal,
|
||||
}
|
||||
var ref Ref
|
||||
var err error
|
||||
if ins.SupersedeSlug != "" {
|
||||
|
||||
Reference in New Issue
Block a user