Retrieval that knows who’s asking#
needtoknow puts your permission system inside RAG retrieval. Every user gets only the chunks they are allowed to read, whatever vector store, permission system or framework you use.
pip install needtoknow
Built for production RAG#
A RAG assistant over company documents is only as safe as its retrieval step. If the search ignores who is asking, an engineer can ask about salaries and get HR documents summarised back. needtoknow sits between your application and your vector store, and closes that gap.
pgvector, Qdrant, Milvus, MongoDB, Weaviate, Redis, LanceDB, Elasticsearch, OpenSearch, Chroma or any LangChain or LlamaIndex store; OpenFGA, SpiceDB, Cerbos, Open Policy Agent or your own rules. The core has no dependencies.
With the filter inside the search, results are the true top-k of what the user may read, not the top-k of everything with the forbidden ones removed.
Errors, timeouts and malformed answers from the permission system raise an error. They never return unfiltered results.
Pre-filtered results are checked again, so a backend that ignores the filter cannot leak, and each result counts the violations it caught.
Every retrieval emits an audit event and, optionally, an OpenTelemetry span, without the query or chunk text.
retrieve and aretrieve share one implementation of the security logic, with native async
clients where the store has them.
How it works#
The authorizer turns the user into a filter (OpenFGA, SpiceDB, Cerbos, Open Policy Agent, or permissions stored on the chunks), or checks candidates one by one.
The filter runs inside the vector store’s own query, so the nearest chunks the user may read come back, however few of the corpus that is.
Every hit is checked again before it reaches the prompt. Anything that cannot be decided raises an error instead.
Your stack, unchanged#
from needtoknow import Document, InMemoryBackend, MetadataACLAuthorizer, PermissionedRetriever, Subject
backend = InMemoryBackend(
[
Document("handbook", "Annual leave is 25 days.", {"allowed_groups": ["*"]}),
Document("salaries", "Salary bands for 2026 ...", {"allowed_groups": ["hr"]}),
]
)
retriever = PermissionedRetriever(backend, MetadataACLAuthorizer())
result = retriever.retrieve("leave salary bands", Subject("bob", groups=["engineering"]), k=3)
print([hit.id for hit in result.hits]) # ['handbook']
from psycopg_pool import ConnectionPool
from needtoknow import PermissionedRetriever, Subject
from needtoknow.authorizers.openfga import OpenFGAAuthorizer
from needtoknow.backends.pgvector import PgVectorBackend
backend = PgVectorBackend(ConnectionPool("postgresql://localhost/app"), embed=embed)
authorizer = OpenFGAAuthorizer("http://localhost:8080", store_id, resource_field="doc_id")
retriever = PermissionedRetriever(backend, authorizer)
result = retriever.retrieve("What are the salary bands?", Subject("alice"))
from needtoknow import MetadataACLAuthorizer, PermissionedRetriever, Subject
from needtoknow.langchain import NeedToKnowRetriever
retriever = NeedToKnowRetriever(retriever=PermissionedRetriever(backend, MetadataACLAuthorizer()), k=4)
docs = retriever.invoke("What changed in Q3 pricing?", subject=Subject("bob", ["sales"]))
from llama_index.core.query_engine import RetrieverQueryEngine
from needtoknow import MetadataACLAuthorizer, PermissionedRetriever, Subject
from needtoknow.llamaindex import NeedToKnowRetriever
permissioned = PermissionedRetriever(backend, MetadataACLAuthorizer())
retriever = NeedToKnowRetriever(permissioned, Subject("bob", ["engineering"]), k=4)
engine = RetrieverQueryEngine.from_args(retriever) # uses your configured LLM
Integrations#
Permissions on the chunks, synced from Google Drive
Permissions on the chunks, or your own rules
LangChain retrievers and agent tools
LlamaIndex retrievers and postprocessors
Haystack retriever component
OpenTelemetry spans
Audit events as JSON lines
Measured, not claimed#
0 forbidden chunks returned in 21,000 benchmark retrievals
0.88–0.97 recall@10 at a million chunks, pre-filtered on pgvector
129/s retrievals on 4 CPUs, a million chunks in PostgreSQL
8–10% cost of a remote permission service, all of it recovered by caching
Every number comes from a reproducible run, with the method and full tables on the Benchmarks page.
Secure by default#
An unavailable or misbehaving permission system stops retrieval; it never widens it. The security model states what is guaranteed and what is assumed.
Every release carries Sigstore signatures, a CycloneDX SBOM and PyPI attestations, published by GitHub Actions without stored credentials.
Actions pinned to commits, CodeQL on the code and the workflows, and a published OpenSSF Scorecard.