How it works

Three acts: find the leak, fix it reversibly, and see the blast radius before you upgrade.

raggate tests retrieval behaviour as a black box. It only needs one thing from your pipeline: the ability to ask it for results as a given identity. Everything else — the verdicts, the evidence, the remedies — is built on top of that single seam.

Act 1

Find — gate the retrieval path

You bring three things: a set of scenarios (an identity plus a query), an identity roster, and a permission snapshot describing who is allowed to read what.

The snapshot is evaluated at run time, never baked into the index. That distinction is the reason the tool can catch anything at all. A chunk carries identifiers — document id, tenant, the grants the index believed at ingest — but the verdict is resolved against whichever snapshot the run names. Comparing a stale index against a fresh policy is precisely the stale-permission case.

Three verdict classes, and the one in the middle matters

ClassMeaning
PASSThe chunk came back and the asking identity is authorized to read it.
LEAKThe chunk came back, reached the context window, and the identity is not authorized. This is what fails the build.
NEAR_MISSA forbidden chunk was recalled by the retriever, but at a rank beyond the final cutoff or below the relevance floor — so it would have been filtered before reaching a model. It is recorded as a measurement, not treated as a pass: the same chunk one rank higher is a leak.

What the sample corpus plants

The bundled two-tenant corpus contains three deliberately planted leak classes, so the gate's ability to catch them is observable rather than asserted:

S1

Revoked, still indexed

An identity loses access to a project between snapshot versions. The chunks remain indexed and still recall for them.

S2

Cross-tenant similarity

One tenant's identity recalls another tenant's near-identical document. Tenant separation is absolute in the oracle: no grant can override it.

S3

Narrowed after the fact

A document that was broadly readable is tightened to a smaller audience. The index does not know, and keeps serving it.

Alongside them sit 10 negative-control scenarios that must produce zero leaks. A gate that only ever says FAIL is not a gate — the controls are how you know it can still say PASS.

Act 2

Fix — the cheapest remedy that actually closes it

Finding a leak is only useful if something can be done about it. raggate carries a remedy catalog ordered safest-first, and deliberately prefers the rungs that change the least.

RungRemedyWhat it does
1pre-filterNarrow retrieval so the forbidden chunk is never a candidate. Deletes nothing. On Elasticsearch this is a filtered alias per tenant.
2alias-rollbackPoint retrieval back at the previous index or embedding version — the natural answer when an upgrade widened the access surface.
4re-ingestRebuild affected documents with corrected metadata.
6tombstoneMake a chunk unretrievable but recoverable.
8hard-deleteRemove it. Last resort, and guarded behind an explicit flag.

The catalog holds eight rungs; these are the load-bearing ones. Which remedies can actually be applied depends on the backend — a remedy needs a mechanism, and raggate refuses the apply rather than pretending when one is missing.

Why cross-tenant leaks can't be fixed by deleting

When one tenant's query recalls another tenant's document, the document is not the problem — it is somebody's legitimate file, correctly indexed. Deleting it would be destroying valid data to fix a retrieval-scope bug. That is why the pre-filter sits at rung 1: it changes who can reach the document rather than whether it exists.

Every apply is reversible, and proves itself. The rollback artifact is written before the first mutation, so an interrupted apply still leaves a working undo. After applying, raggate confirms the change is actually visible to the backend, re-runs the identical gate through the new configuration, and asserts the leak count reached zero. If it did not, you get the exact rollback command instead of a success message.
Act 3

Diff — what would this upgrade expose?

Embedding upgrades are sold as recall improvements. They are also access-surface changes, and nothing in a relevance benchmark will tell you so.

The access-surface diff holds permissions constant and compares the reachable set under two configurations — the current embedding and the candidate. Anything that becomes reachable and is forbidden under the snapshot is reported.

$ raggate diff --corpus corpus --snapshot corpus/acl_snapshots/v2.yaml \
    --scenarios corpus/scenarios --config-a baseline --config-b broad

newly reachable: 17 docs; of which FORBIDDEN: 10
HEADLINE: the embedding change made 10 forbidden documents reachable with permissions unchanged
scope: probe-scope — compared 26 probe queries, not a corpus-wide scan;
       a clean result covers these queries only

Verbatim from bash scripts/demo.sh on the sample corpus. Note the tool reports its own scope limit in the output — a clean diff over 26 probes is evidence about those 26 probes, not a proof about your corpus.

Scope honesty is part of the design. raggate would rather tell you what it did not check than let a green result imply more than it earned. The same applies to a passing gate: it covers the identities and queries you supplied.
Where it plugs in

One interface, and adapters for the stores people actually run.

Everything above sits behind a single seam: retrieve(query, identity, top_k). If your pipeline can answer that, raggate can gate it.

Bring your own retriever

Expose your pipeline — filters, reranking, query rewriting and all — behind one HTTP endpoint and point the gate at it. The contract is a documented request and response shape; a shim is typically a few dozen lines. What gets tested is your real pipeline, not a reimplementation of it.

If your endpoint omits a relevance score, the gate treats the chunk as fully relevant — a forbidden chunk cannot dodge the threshold by declining to report one.

Or use a bundled adapter

Ten vector-store adapters ship in the repository, including sqlite-vec for local runs, Elasticsearch and OpenSearch, pgvector, Qdrant, Milvus, Weaviate, Redis and Azure AI Search. Scores are recomputed locally as exact cosine so a threshold means the same thing on every backend.

Adapters vary in what they can apply: some support the full remedy loop, others are detection-and-plan only. The CLI tells you which up front.