Skip to content
Harness Engineering2026-09-108 min readRev. 2026-09-30

Filter Retrieval by Permission Before Anything Reaches the Prompt

Permission-aware retrieval keeps unauthorized passages out of the ranked candidates, not just out of the answer. Test it on the prompt bytes, and measure what post-filtering costs in recall@k.

Key takeaways

  • Relevance and authorization are separate filters; embedding, search and reranking answer only the first.
  • The acceptance rule is about what reached the model: an unauthorized passage in the reranked context is a failure even if the answer never mentions it.
  • Leakage is tested exhaustively on the prompt bytes and must be zero; recall@k is the quality metric you trade against it.
  • Filtering after a top-K search costs recall: if a user can see 5% of the index, K must be about 181 for a 95% chance of five authorized results.
  • Caches and stale index metadata are the two usual ways a correct filter gets bypassed.

Rod Rivera

Author

Filter Retrieval by Permission Before Anything Reaches the Prompt

Rod's note: read with a pencil; the margins are for you.

The passage the answer never mentioned

A lab runs an internal research assistant over its document store: the cluster wiki, experiment notes and eval reports. Most of it is open to everyone. Some is not. An eval report on an unreleased model is restricted to the eval team. Ana is on that team. Bo is not.

Bo asks, "What did the latest eval show?" The pipeline embeds the query, pulls the top candidates by vector similarity, reranks them, and hands the best few to the model. The model writes a careful answer about Bo's own notes and never mentions the restricted report. Did the system behave?

You cannot tell from the answer. The question is what entered the context window. If the restricted report was among the reranked passages, the boundary was crossed the moment those bytes went into the prompt. A model choosing not to repeat something is not a permission check, and nothing guarantees it will choose the same way on the next call.

Relevance and authorization are different filters

A retrieval pipeline has three relevance stages: embed the query, search an index for near neighbors, and rerank the candidates with a model that scores each query-passage pair. Chapter 6 builds the first two from scratch and measures them. NVIDIA's NeMo Retriever reranking documentation describes the third as a relevance-scoring stage. None of the three asks whether this caller may see this passage.

So authorization is its own filter, driven by its own data: who is asking, established by the authentication layer, and which documents that caller may read right now, from an authoritative permission source. A valid access token does not answer the second question. Token validation says the request may reach the service; it does not map a caller to documents. Checking the caller's authority, not the service account's covers the identity half.

Where the filter sits

Constrain the search itself to documents the caller may read, then recheck the IDs before anything is sent to a reranker or a model. If your vector store supports an enforced filter on the query, use it. A hosted reranker that receives a passage has already received it; filtering after that is too late for the reranker boundary.

Current permissions constrain retrieval before reranking. Query and authenticated caller; Resolve current permissions; Search authorized documents only; Recheck returned document IDs; Rerank permitted passages; Assemble context
Current permissions constrain retrieval before reranking

Here is the rule as a fixture. The documents carry obvious markers so the test can inspect the prompt bytes, not only the IDs:

python
from dataclasses import dataclass


@dataclass(frozen=True)
class Doc:
    doc_id: str
    text: str


# Synthetic documents with obvious markers, never real restricted material.
DOCS = [
    Doc("wiki-cluster", "How to request GPUs on the shared cluster."),
    Doc("eval-restricted", "MARKER-EVAL-TEAM-ONLY unreleased benchmark scores."),
    Doc("bo-notes", "MARKER-BO-ONLY draft notes on the tokenizer ablation."),
]
PERMISSIONS = {  # the authoritative source, read at request time
    "ana": {"wiki-cluster", "eval-restricted"},
    "bo": {"wiki-cluster", "bo-notes"},
}


def allowed_ids(user):
    if user not in PERMISSIONS:
        raise PermissionError("authenticated caller required")
    return PERMISSIONS[user]


def search(query, docs, k):
    return docs[:k]  # stand-in ranking: every doc is "relevant" in this fixture


def build_context(query, user, k=3):
    allowed = allowed_ids(user)
    candidates = search(query, [d for d in DOCS if d.doc_id in allowed], k)  # filter before ranking
    context = [d for d in candidates if d.doc_id in allowed_ids(user)]  # recheck before the prompt
    return context


bo = build_context("what did the latest eval show?", "bo")
print([d.doc_id for d in bo])
prompt = "\n".join(d.text for d in bo)
assert "MARKER-EVAL-TEAM-ONLY" not in prompt
assert {d.doc_id for d in bo} <= allowed_ids("bo")
print("no restricted marker in Bo's prompt bytes")
text
['wiki-cluster', 'bo-notes']
no restricted marker in Bo's prompt bytes

I ran it with Python 3.12 on September 30, 2026. It has no embedding model and no vector store; the ranking is a stand-in. What it shows is where the checks go and what the assertions look at: a subset check on IDs against current permissions, and a search for the forbidden marker in the actual prompt text. An ID check alone cannot catch text attached to the wrong ID.

A passing local test is not a deployment guarantee

The fixture shows the filtering logic. It does not show that your vector store applies its filter correctly, that your reranker adapter forwards only what it is given, or that a shared cache respects the boundary under load. Run the same marker test through the deployed path.

Two metrics, and they are not symmetric

Permission-aware retrieval has a safety metric and a quality metric, and they behave differently.

Leakage is the number of (query, caller) pairs in which any unauthorized passage reached the context. It is not estimated with an interval, because the target is zero and a single case is a failure. Test it exhaustively over your fixture: every test caller, every query, with a marker in every restricted document, and assert on the prompt bytes.

Recall@k is the share of relevant, authorized passages that appear in the top k. Chapter 5 derives it with precision@k and reciprocal rank, and names why it matters most for agents: a relevant record outside the top k never reaches the model. The filter can cost recall, and how much depends on where it runs.

What post-filtering costs

The tempting design is to search the whole index for the top K, then drop what the caller may not see. Suppose the caller can read a fraction f of the index, and suppose, as a first approximation, that permission is independent of relevance. Then the number of authorized passages among the top K is binomial with mean f·K.

Take f = 0.05 and a context that needs k = 5 passages. The mean says K = 5 / 0.05 = 100. But the mean is not enough: with K = 100 the chance of getting at least five authorized passages is only 0.564. The smallest K that gets five with 95% probability is 181.

python
from math import comb


def p_at_least(k, K, f):
    return 1 - sum(comb(K, i) * f**i * (1 - f) ** (K - i) for i in range(k))


for K in (100, 150, 200):
    print(K, round(p_at_least(5, K, 0.05), 3))
K = 5
while p_at_least(5, K, 0.05) < 0.95:
    K += 1
print("smallest K with 95% chance of 5 authorized:", K)
text
100 0.564
150 0.874
200 0.974
smallest K with 95% chance of 5 authorized: 181

That is arithmetic under an assumption, not a measurement of any system. In real stores permission and relevance are correlated: people mostly search their own team's documents, which helps; a restricted report can also be the nearest neighbor for exactly the query that matters, which hurts. The number tells you the direction: post-filtering makes recall depend on how much of the index each user can see, and the users who see least lose the most. Pre-filtering inside the search keeps k authorized results by construction, at whatever cost your index charges for filtered search.

The measurement protocol

This article reports no measured recall. Here is how to get yours.

Build a labeled fixture with permissions

A few hundred documents, each with an access list, a unique marker string in each restricted one, and 30 or more queries per test caller with their relevant authorized documents labeled in advance.

Run the leakage check exhaustively

For every caller and query, capture the reranker input and the final prompt, and assert that no marker from a document outside that caller's current permissions appears. The pass condition is zero, with no interval.

Compare two designs on recall@k

Pre-filter inside the search, and post-filter a top-K list for two or three values of K. Report recall@5 per caller group, with a Wilson interval on each proportion, since each query's hit or miss is a trial.

Repeat after a permission change and a cache hit

Revoke one document from one caller, rerun without rebuilding the index, then serve a cached candidate list to a different caller. Both must still show zero leakage.

Fresh and cached candidates both pass current caller authorization before their passages enter reranking or generation context.. Fresh retrieval candidates; Check current caller permissions; Cached candidates with IDs and versions; Permitted passages only; Reranker and generation context
Fresh and cached candidates both pass current caller authorization before their passages enter reranking or generation context.

The two usual bypasses

Caches. An answer or candidate list cached for Ana under a query-only key must not be served to Bo for the same query. Include the caller and an authorization revision in the key, and still recheck current permissions before the passages leave the retrieval service. Invalidating cached answers when authority changes works through the revision design and the race with an in-flight request.

Stale index metadata. If permissions are copied into the index at ingestion time, a revocation after indexing leaves the old grant in place until the next rebuild. Read permissions from the authoritative source at query time, or version them and refuse results whose version is stale. If the permission source is unavailable, deny; do not treat a failed lookup as unrestricted access.

Quick check — Bo's answer never mentions the restricted eval report, but a debug log shows the report's ID in the reranked list handed to the model. Did the access check pass?

Check the decision

Why this matters in a lab

Retrieval over internal documents is one of the first agent systems many applied-AI teams build, for their own staff and for customers. An interviewer who asks you to design one is likely to probe two things: whether retrieval quality is measured (recall@k on a labeled set, not a demo) and whether access control is enforced before the prompt (not by instruction). The strong answer names both metrics, says why leakage is tested exhaustively and recall statistically, and explains what post-filtering costs with the arithmetic above.

What would prove this wrong

The filter has failed if any of these happen in your test: an ID outside current permissions appears in the search output, the reranker input or the final context; a restricted marker appears in the prompt bytes; a cached list for one caller is served to another without the recheck; or a revoked document still surfaces because the filter trusted index metadata. The recall argument fails if permission and relevance are so correlated in your data that post-filtering loses nothing. Measure it before you assume either way.

Read Chapter 6: embeddings and vector search, measured

Build word vectors from scratch, then measure a real embedding model against BM25 on paraphrased questions with recall@3.

Learn the mechanism. Test the claim.

Join the Prof Rod newsletter for one educational lesson a week, with worked examples attached. It is free to register for and separate from the Zero Employee community.