feat(mcp): brain_read + brain_edit, contained to the sync source (brain#4) (#96)
brain_read returns a note verbatim; brain_edit is str_replace with an exactly-once match, operating on the tree brain-sync commits from. An adversarial design review before implementation changed three things: 1. Governance files are not in the model-facing tool. Brain content is ingested from the outside world and reaches a model's context, and that model would hold the editor — one poisoned page could rewrite the CLAUDE.md a .dispatch-allow agent later loads as instructions. And brain-sync merges with --strategy-option=ours, so the live tree beats a human's corrective push on the conflicting lines. CLAUDE.md now goes through brain_edit_root: de-surfaced from tools/list and gated on the local static principal. The scopes are disjoint — the privileged door has the smaller reach. 2. Reads needed the same allowlist, and brain_get already lacked it: it returned verbatim session transcripts under archive/ by path. A test proved the exposure live before the fix. 3. Canonicalise before allowlisting — knowledge/../.git/hooks/post-commit would otherwise reach a hook that brain-sync executes in a pod holding the Gitea push credentials. Plus overlapping match counting, UTF-8 validation both ways, refusal while git is mid-operation, flock with the read inside the lock, atomic same-dir rename, and reindex only when frontmatter is touched. Refs brain#4. Co-authored-by: Mathias <mthbqv@gmail.com>
This commit was merged in pull request #96.
This commit is contained in:
@@ -59,6 +59,7 @@ func TestServerToolsList(t *testing.T) {
|
||||
assert.ElementsMatch(t, []string{
|
||||
"brain_query", "brain_write", "brain_update", "brain_get",
|
||||
"brain_pending", "brain_promote",
|
||||
"brain_read", "brain_edit",
|
||||
"brain_tunnel",
|
||||
"brain_ingest_raw", "brain_ingest",
|
||||
"brain_answer", "brain_classify", "brain_graph", "brain_context",
|
||||
@@ -69,6 +70,13 @@ func TestServerToolsList(t *testing.T) {
|
||||
// a caller verb is cognitive load with no decision behind it (ADR-0012
|
||||
// rule 4, #78). The capability stays reachable — see the admin test below.
|
||||
assert.NotContains(t, names, "brain_index")
|
||||
|
||||
// brain_edit_root is de-surfaced for a sharper reason than brain_index:
|
||||
// brain content is ingested from the outside world and reaches a model's
|
||||
// context, so a listed governance-file editor would be a durable
|
||||
// prompt-injection path — one poisoned page rewriting the CLAUDE.md a
|
||||
// .dispatch-allow agent later loads as instructions.
|
||||
assert.NotContains(t, names, "brain_edit_root")
|
||||
}
|
||||
|
||||
func TestBrainIndexStaysCallableForAdminUseAfterDeSurfacing(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user