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>
Removes brain_write legacy domain/type/knowledge-path params (ADR-0012) and migrates the previously-missed cmd/hyperguild CLI caller (hyperguild brain write) from <type> <slug> to <wing> <hall> <slug>, fixing a 100%% regression the first review round caught. Adds TestWrite_CLIPayloadRoundTrips exercising the real Handler.Write over HTTP to pin the wire contract.
Closes#79.
Co-authored-by: Mathias <mthbqv@gmail.com>
Every brain write accepted [[targets]] that resolve to nothing — skill
names, repo names, agent-local memory-file slugs — and each one became a
permanent [high] broken-link finding. The brain-audit digest grew 19 -> 63
high findings between 2026-08-26 and 2026-09-16; 53 of the 63 were exactly
this, and none are fixable by brain-gardener's BrokenLinkFixer, which only
repoints edit-distance-1 renames. There is no target to repoint to.
StripUnresolvedLinks resolves each link against knowledge/ + wiki/ using
brain-gardener's own resolver semantics (bare slug or wing/hall/slug path
form, code spans excluded) and unwraps the misses to plain text, reporting
them as Ref.Unlinked / the MCP `unlinked` field.
Unwrap rather than reject: a refused write on claudewatcher's unattended
ingest path is a lost learning, and the author's words are worth keeping
even when the link is not. An empty corpus is a no-op, so a misconfigured
brainDir cannot strip every link in a note.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ai-sessions#13: capture's `summary` field wrote a frontmatter shape
(title/harness/fidelity/captured_at/repos_touched) that ai-sessions'
own extract/audit pipeline can't parse -- silently invisible to that
repo's own audit tooling, and bypassing its redaction + Stage2
completeness gates by construction. Only 5 files ever landed this way
over 3 weeks; the summary capability's whole value (speed) fights
ai-sessions' whole value (redacted, audited, complete), so kill it
rather than build a second parse branch.
capture now persists insights -> brain and action items -> Gitea
tickets only. Removes Summary/SummaryResult/SummaryWriter and all
wiring (service, REST body, MCP tool schema); close-session updated to
stop assembling a summary payload. specs/capture-*.md kept as historical
record with a superseded note -- the feature shipped and is documented,
just no longer current behaviour.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPdSHbp9Utb2hPFm9wDG59
Splits the audit port into Reserve (before any write) + Record (after),
so "confidential + sink-down → refuse before any write" is literally true
even though the audit record — which lists what landed — can only be
written afterwards.
- AuditSink.Reserve(ctx, level) → AuditOutcome | error. The error path
refuses the capture before writing: confidential + central sink down,
or the all-tiers floor (nothing can record).
- AuditSink.Record(ctx, entry, outcome) persists per the reserved outcome.
- Service: I5 gate runs after the I1 gate and after the dry-run
short-circuit (dry-run never probes the sink). AuditBuffered surfaces on
the receipt. New ErrAuditUnavailable sentinel (→ HTTP 503).
The tier→behaviour decision lives in the sink impl (#54's DegradingSink),
not the service — the service just honours Reserve's verdict.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#52's IssueTracker spec is CloseIssue(repo, number, comment). Refine the
#51 port signature to match and have the service pass the ticket body as
the closing comment (empty ⇒ close only). Keeps the close-with-comment
flow first-class rather than forcing two separate ticket items.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Clean-Architecture core of the capture capability (#49b). Pure
orchestration over ports — no HTTP, no live Gitea, no audit I/O — fully
unit-tested against fakes before any adapter exists.
- Ports: BrainStore (#45 write/update/get), IssueTracker, SummaryWriter,
ClassificationPolicy (satisfied by #50's classification.Config),
AuditSink. Entities: Insight, Ticket, Summary, CaptureContext,
CaptureInput, CaptureReceipt.
- CaptureService.Capture: validate-before-write (fail-closed), resolve
effective classification (stricter of declared vs target-derived;
under-declaration logged as a security event), orchestrate insights
(write/supersede) → tickets → summary best-effort, emit a request-level
audit record of exactly what landed, return a partial-aware receipt.
- dry_run short-circuits after validation, writes nothing (not even audit).
Out of scope here, layered on later: the I1 origin sovereignty gate (#53,
needs the server-derived principal) and the classification-aware audit
degradation/refusal (#54). "Effective" is folded into the service as
classification.Stricter rather than a port method — the stricter-wins
rule is use-case policy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>