fix(brain): close round-3 review findings on provenance
- B1: search.extractProvenance locates the frontmatter with
frontmatter.Split (fence at column 0), so an indented "---" inside a
block scalar no longer hides provenance from brain_query.
- B2: brain_edit refuses to edit without a snapshot, and if the stamp
cannot be verified restores the note and reports failure; a failed
restore is reported as exactly that, never as "rolled back".
- B3: internal/provenance gets direct tests for the whole-document
backstop (a stamp that cannot read back is refused), standalone group
decoding, duplicates, lone CR, tab continuations and unclosable
fences. Removing verify or the restore now fails the suite.
- Principals YAML would read as non-strings (true, 123, null, ~) are
quoted instead of refused. An unclosable fence ("---x") is refused
rather than demoted into the body. writeHallNote creates the hall
directory only after the note passes, so a refused write leaves
nothing behind (brain#21).
- MCP brain_ingest (stub LLM) asserted to stamp the caller.
Refs #145 #144
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ package mcp_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"os"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/capturehttp"
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/classification"
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/mcp"
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/pipeline"
|
||||
)
|
||||
|
||||
// Escapes found by the PR #147 gatekeeper review. Each test drives the real
|
||||
@@ -260,3 +262,64 @@ func TestToolsCallAuditErrorOutcomeCarriesNoArgumentValues(t *testing.T) {
|
||||
}
|
||||
assert.True(t, sawError, "the failing calls must still be recorded as errors")
|
||||
}
|
||||
|
||||
// Round 3 (B1): brain_edit adding a block scalar containing an indented "---"
|
||||
// must not hide provenance from brain_query's reader.
|
||||
func TestBrainEditBlockScalarFenceDoesNotHideProvenanceFromQuery(t *testing.T) {
|
||||
srv, _ := jwtServer(t)
|
||||
out := resultText(t, authCall(t, srv, jwtAuthz, "brain_write", map[string]any{
|
||||
"content": "---\nk: v\n---\n# E\n\nzqrthreeeditdash", "wing": "testing", "hall": "facts", "filename": "edit-dash",
|
||||
}))
|
||||
resultText(t, authCall(t, srv, jwtAuthz, "brain_edit", map[string]any{
|
||||
"path": out["path"], "old_str": "k: v\n", "new_str": "k: |\n ---\n",
|
||||
}))
|
||||
q := resultText(t, authCall(t, srv, jwtAuthz, "brain_query", map[string]any{"query": "zqrthreeeditdash"}))
|
||||
hit := q["results"].([]any)[0].(map[string]any)
|
||||
assert.Equal(t, "jwt", hit["origin"])
|
||||
assert.Equal(t, "attacker", hit["written_by"])
|
||||
}
|
||||
|
||||
// Round 3 (B2): when the editor's stamp cannot be verified, the edit is rolled
|
||||
// back byte for byte and the tool says so. A JWT subject with an invalid UTF-8
|
||||
// byte cannot round-trip through YAML, which forces that path.
|
||||
func TestBrainEditRollsBackWhenTheStampCannotBeVerified(t *testing.T) {
|
||||
dir := newBrainRepo(t)
|
||||
cfg, err := classification.Load(dir)
|
||||
require.NoError(t, err)
|
||||
svc := capture.NewService(brainstore.New(dir), capFakeTracker{}, cfg, audit.NewSlogSink(nil))
|
||||
srv := mcp.NewServer(dir, nil, nil, nil)
|
||||
srv.WithCapture(svc, capFakeValidator{subject: "bad\xff"}, capStaticTok, "local-cli", capturehttp.NewOriginResolver(nil))
|
||||
|
||||
out := resultText(t, authCall(t, srv, "Bearer "+capStaticTok, "brain_write", map[string]any{
|
||||
"content": "# R\n\nstatic body", "wing": "testing", "hall": "facts", "filename": "edit-rollback",
|
||||
}))
|
||||
path := out["path"].(string)
|
||||
before, err := os.ReadFile(filepath.Join(dir, path))
|
||||
require.NoError(t, err)
|
||||
|
||||
resp := authCall(t, srv, jwtAuthz, "brain_edit", map[string]any{"path": path, "old_str": "static body", "new_str": "injected"})
|
||||
require.NotNil(t, resp["error"], "the edit must be reported as failed")
|
||||
assert.Contains(t, resp["error"].(map[string]any)["message"], "rolled back")
|
||||
after, err := os.ReadFile(filepath.Join(dir, path))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, string(before), string(after), "the note must be byte-identical to before the edit")
|
||||
}
|
||||
|
||||
// MCP brain_ingest (the LLM path) attributes its pages to the caller.
|
||||
func TestBrainIngestViaMCPStampsCaller(t *testing.T) {
|
||||
dir := newBrainRepo(t)
|
||||
cfg, err := classification.Load(dir)
|
||||
require.NoError(t, err)
|
||||
svc := capture.NewService(brainstore.New(dir), capFakeTracker{}, cfg, audit.NewSlogSink(nil))
|
||||
stub := pipeline.Config{Complete: func(context.Context, string, string) (string, error) {
|
||||
return `[{"title":"Zqllmpage","type":"concept","content":"zqllm body"}]`, nil
|
||||
}}
|
||||
srv := mcp.NewServer(dir, &stub, nil, nil)
|
||||
srv.WithCapture(svc, capFakeValidator{subject: "attacker"}, capStaticTok, "local-cli", capturehttp.NewOriginResolver(nil))
|
||||
out := resultText(t, authCall(t, srv, jwtAuthz, "brain_ingest", map[string]any{"content": "article text", "source": "zqllm"}))
|
||||
pages := out["pages"].([]any)
|
||||
require.NotEmpty(t, pages)
|
||||
for _, p := range pages {
|
||||
assertProvenance(t, srv, dir, p.(string), "jwt", "attacker")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user