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:
@@ -95,6 +95,18 @@ func (f *frontmatter) set(key, value string) {
|
||||
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
|
||||
// empty frontmatter renders to the empty string so bodies without a
|
||||
// header stay header-less.
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -46,6 +46,11 @@ type UpdateNoteOptions struct {
|
||||
Slug string
|
||||
Content string // new full body (whole-note replace)
|
||||
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
|
||||
@@ -122,12 +127,19 @@ func UpdateNote(brainDir string, opts UpdateNoteOptions) (relPath, contentHash,
|
||||
// overwrites below to save the other two keeps the invariant
|
||||
// explicit at the one place it can be silently broken by a future
|
||||
// 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
|
||||
}
|
||||
fm.set(l.key, l.value)
|
||||
}
|
||||
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)
|
||||
if opts.Reason != "" {
|
||||
fm.set("supersede_reason", opts.Reason)
|
||||
|
||||
@@ -49,10 +49,12 @@ func (s *Store) Write(ctx context.Context, n capture.Note) (capture.Ref, error)
|
||||
n.Content = content
|
||||
|
||||
relPath, err := api.WriteNote(s.brainDir, api.WriteNoteOptions{
|
||||
Content: n.Content,
|
||||
Filename: n.Filename,
|
||||
Wing: n.Wing,
|
||||
Hall: n.Hall,
|
||||
Content: n.Content,
|
||||
Filename: n.Filename,
|
||||
Wing: n.Wing,
|
||||
Hall: n.Hall,
|
||||
Origin: n.Origin,
|
||||
WrittenBy: n.WrittenBy,
|
||||
})
|
||||
if err != nil {
|
||||
return capture.Ref{}, err
|
||||
@@ -74,7 +76,7 @@ func (s *Store) Update(ctx context.Context, slug string, n capture.Note) (captur
|
||||
}
|
||||
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, "/") {
|
||||
opts.Path = slug
|
||||
} else {
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -104,6 +104,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// caller may have tried to put in the body.
|
||||
in.Context.Principal = principal
|
||||
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)
|
||||
switch {
|
||||
|
||||
@@ -12,13 +12,7 @@ import (
|
||||
// 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) {
|
||||
principal, origin := "", "unknown"
|
||||
if info, ok := ctx.Value(principalKey).(principalInfo); ok {
|
||||
principal, origin = info.principal, "jwt"
|
||||
if info.viaStatic {
|
||||
origin = "static"
|
||||
}
|
||||
}
|
||||
origin, principal := provenanceFrom(ctx)
|
||||
attrs := []any{
|
||||
"tool", name,
|
||||
"principal", principal,
|
||||
|
||||
@@ -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
|
||||
// handle {id, path, content_hash} comes back from the store; path is kept
|
||||
// for backward compatibility.
|
||||
origin, writtenBy := provenanceFrom(ctx)
|
||||
ref, err := s.store.Write(ctx, capture.Note{
|
||||
Content: a.Content,
|
||||
Filename: a.Filename,
|
||||
Wing: a.Wing,
|
||||
Hall: a.Hall,
|
||||
Content: a.Content,
|
||||
Filename: a.Filename,
|
||||
Wing: a.Wing,
|
||||
Hall: a.Hall,
|
||||
Origin: origin,
|
||||
WrittenBy: writtenBy,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -304,11 +307,14 @@ func (s *Server) brainUpdate(ctx context.Context, args json.RawMessage) (json.Ra
|
||||
if a.Path != "" {
|
||||
slug = a.Path
|
||||
}
|
||||
origin, writtenBy := provenanceFrom(ctx)
|
||||
ref, err := s.store.Update(ctx, slug, capture.Note{
|
||||
Content: a.Content,
|
||||
Wing: a.Wing,
|
||||
Hall: a.Hall,
|
||||
Reason: a.Reason,
|
||||
Content: a.Content,
|
||||
Wing: a.Wing,
|
||||
Hall: a.Hall,
|
||||
Reason: a.Reason,
|
||||
Origin: origin,
|
||||
WrittenBy: writtenBy,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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.
|
||||
in.Context.Principal = info.principal
|
||||
in.Context.Origin = s.capture.resolver.Resolve(info.principal, info.viaStatic)
|
||||
in.Context.AuthPath, _ = provenanceFrom(ctx)
|
||||
|
||||
rec, err := s.capture.svc.Capture(ctx, in)
|
||||
if err != nil {
|
||||
@@ -99,3 +100,17 @@ func (s *Server) brainCapture(ctx context.Context, args json.RawMessage) (json.R
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -48,6 +48,11 @@ type Result struct {
|
||||
// (infra#72). Read from frontmatter when present, otherwise
|
||||
// inferred from the parent directory.
|
||||
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
|
||||
@@ -146,6 +151,7 @@ func QueryContext(ctx context.Context, brainDir string, opts QueryOptions) ([]Re
|
||||
rel = filepath.ToSlash(rel)
|
||||
wing, hall := extractWingHall(string(content), rel)
|
||||
tier := extractTier(string(content), rel)
|
||||
origin, writtenBy := extractProvenance(string(content))
|
||||
results = append(results, Result{
|
||||
Path: rel,
|
||||
Title: extractTitle(string(content), d.Name()),
|
||||
@@ -154,6 +160,9 @@ func QueryContext(ctx context.Context, brainDir string, opts QueryOptions) ([]Re
|
||||
Wing: wing,
|
||||
Hall: hall,
|
||||
Tier: tier,
|
||||
|
||||
Origin: origin,
|
||||
WrittenBy: writtenBy,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
@@ -272,6 +281,7 @@ func hydrate(brainDir, relPath string) (Result, error) {
|
||||
}
|
||||
wing, hall := extractWingHall(string(content), relPath)
|
||||
tier := extractTier(string(content), relPath)
|
||||
origin, writtenBy := extractProvenance(string(content))
|
||||
return Result{
|
||||
Path: relPath,
|
||||
Title: extractTitle(string(content), filepath.Base(relPath)),
|
||||
@@ -279,9 +289,45 @@ func hydrate(brainDir, relPath string) (Result, error) {
|
||||
Wing: wing,
|
||||
Hall: hall,
|
||||
Tier: tier,
|
||||
|
||||
Origin: origin,
|
||||
WrittenBy: writtenBy,
|
||||
}, 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
|
||||
// filters. Validates hall against the closed vocabulary when set.
|
||||
func resolveRoots(brainDir, wing, hall string) ([]string, error) {
|
||||
|
||||
Reference in New Issue
Block a user