36b757fabf006b5fb512441ae04a403fb1026ffe
3
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
36b757fabf |
feat(mcp): add brain_read + brain_edit, contained to the sync source (brain#4)
brain_read returns a note verbatim; brain_edit is str_replace with an exactly-once match. Both operate on INGEST_BRAIN_DIR, which is the pod's mount of /var/lib/supervisor/brain — the tree brain-sync commits from, so an edit reaches gitea on the next cycle rather than landing somewhere the sync cannot see it (brain#2). An adversarial design review before implementation changed three things, each of which would otherwise have shipped: 1. Governance files are NOT in the model-facing tool. The original plan allowed root *.md so CLAUDE.md had an MCP path at all — the absence of one is what pushed a session to edit via the gitea REST API on 2026-06-16 and wedge the sync. But brain content is ingested from the outside world, reaches a model's context via brain_query, and that model would hold the editor: one poisoned page could rewrite the CLAUDE.md a .dispatch-allow agent later loads as instructions. Worse, brain-sync merges with --strategy-option=ours, so the live tree wins against a human's corrective push on exactly the conflicting lines. CLAUDE.md now goes through brain_edit_root: de-surfaced from tools/list (the brain_index precedent, ADR-0012 rule 4) and gated on the local static principal, so a claude.ai-origin session is refused. The two scopes are disjoint — the privileged door has the smaller reach. 2. Reads needed the same allowlist, and brain_get already lacked it. api.ReadNote resolves any brain-relative path and parseFrontmatter returns the whole file when there is no fence, so brain_get reads verbatim session transcripts under archive/ (up to 500 KB, never BM25-indexed, so nobody thinks of them as reachable). A test proves the exposure was live; the same Resolve now gates brain_get. 3. Canonicalise before allowlisting. A prefix check on the raw argument accepts knowledge/../.git/hooks/post-commit, which Clean collapses to a git hook that brain-sync executes inside a pod holding the Gitea push credentials. Traversal, symlink escape, dotfiles, .git/, archive/, sessions/ and raw/ are refused by a static backstop that does not depend on the allowlist being right. There were no traversal tests in this repo before. Also from the review: overlapping match counting (bytes.Count says "aa" occurs once in "aaa", so a Count-based uniqueness check accepts an ambiguous edit); UTF-8 validation on both sides, because encoding/json substitutes U+FFFD and would hand back bytes the file does not contain, making every derived old_str unmatchable; refusal while .git/MERGE_HEAD or index.lock exists, since an edit racing brain-sync's merge is either hard-reset away or committed as a wrongly resolved conflict; flock plus a per-path mutex with the read taken inside the lock, because two pods share the hostPath during a RollingUpdate and uniqueness evaluated against a stale read loses an update while reporting success; same-dir *.tmp + rename + chmod 0644 + fsync of file and parent; and reindex only when the edit touches frontmatter, since BM25 walks live and embeddings are mtime-driven. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m |
||
|
|
1cea2c9f78 |
feat(capture): remove summary->ai-sessions write path
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 |
||
|
|
7cf5bc221d |
feat(mcp): capture relay tool — MCP door for non-library harnesses (#55)
Adds the `capture` MCP tool: the #55 relay for harnesses that cannot run the use-case in-process (claude.ai Chat/Cowork/Design, Crush, Pi, LLM Council). They reach it through the existing /mcp OAuth connector. - Thin: forwards to the SAME CaptureService as POST /capture; holds no state and retains nothing beyond the I5 audit record. The containment properties accepted in infra security-baseline (I2 ledger) hold by construction. - Per-principal: ServeHTTP re-derives the caller's principal from the Bearer header (the chassis middleware gates but discards it) and stashes it in context; the tool resolves the trust-zone origin from it. A caller-asserted harness/origin in the body is ignored — origin is server-derived, so the I1 confidential refusal still fires for us-nexus callers (claude.ai), and sovereign-allowlisted JWT principals pass. - Registered only when WithCapture is wired (all three sites: tools(), handleCall, package doc); main wires REST + MCP from the same service, resolver, and credentials. Tests: listed-only-when-wired, forwards-via-static-principal, confidential-via-us-nexus-refused, confidential-via-sovereign-allowed, unauthenticated-rejected, caller-cannot-forge-origin. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |