needtoknow 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. needtoknow-1.0.0/.gitignore +13 -0
  2. needtoknow-1.0.0/CHANGELOG.md +107 -0
  3. needtoknow-1.0.0/LICENSE +21 -0
  4. needtoknow-1.0.0/PKG-INFO +297 -0
  5. needtoknow-1.0.0/README.md +250 -0
  6. needtoknow-1.0.0/needtoknow/__init__.py +41 -0
  7. needtoknow-1.0.0/needtoknow/authorizers/__init__.py +1 -0
  8. needtoknow-1.0.0/needtoknow/authorizers/opa.py +112 -0
  9. needtoknow-1.0.0/needtoknow/authorizers/openfga.py +164 -0
  10. needtoknow-1.0.0/needtoknow/authz.py +104 -0
  11. needtoknow-1.0.0/needtoknow/backends/__init__.py +5 -0
  12. needtoknow-1.0.0/needtoknow/backends/chroma.py +156 -0
  13. needtoknow-1.0.0/needtoknow/backends/memory.py +60 -0
  14. needtoknow-1.0.0/needtoknow/backends/pgvector.py +189 -0
  15. needtoknow-1.0.0/needtoknow/caching.py +156 -0
  16. needtoknow-1.0.0/needtoknow/events.py +76 -0
  17. needtoknow-1.0.0/needtoknow/filters.py +128 -0
  18. needtoknow-1.0.0/needtoknow/langchain.py +100 -0
  19. needtoknow-1.0.0/needtoknow/llamaindex.py +96 -0
  20. needtoknow-1.0.0/needtoknow/otel.py +55 -0
  21. needtoknow-1.0.0/needtoknow/py.typed +0 -0
  22. needtoknow-1.0.0/needtoknow/retriever.py +327 -0
  23. needtoknow-1.0.0/needtoknow/types.py +45 -0
  24. needtoknow-1.0.0/pyproject.toml +71 -0
  25. needtoknow-1.0.0/tests/helpers.py +116 -0
  26. needtoknow-1.0.0/tests/test_backends.py +161 -0
  27. needtoknow-1.0.0/tests/test_caching_events.py +189 -0
  28. needtoknow-1.0.0/tests/test_chroma_probe.py +44 -0
  29. needtoknow-1.0.0/tests/test_core.py +202 -0
  30. needtoknow-1.0.0/tests/test_filters.py +63 -0
  31. needtoknow-1.0.0/tests/test_frameworks.py +118 -0
  32. needtoknow-1.0.0/tests/test_opa.py +156 -0
  33. needtoknow-1.0.0/tests/test_openfga.py +158 -0
  34. needtoknow-1.0.0/tests/test_properties.py +55 -0
  35. needtoknow-1.0.0/tests/test_translators.py +89 -0
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ .env
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ .coverage
9
+ htmlcov/
10
+ dist/
11
+ build/
12
+ docs/_build/
13
+ *.local.md
@@ -0,0 +1,107 @@
1
+ # Changelog
2
+
3
+ All notable changes are recorded here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [Semantic Versioning](https://semver.org/). From 1.0 on, the public API is stable.
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [1.0.0] - 2026-10-16
10
+
11
+ The first stable release. The public API in the
12
+ [API reference](https://muzakkirhussain011.github.io/needtoknow/api.html) now follows semantic
13
+ versioning. Everything in 1.0.0rc1 below is included; these are the changes since the release
14
+ candidate.
15
+
16
+ ### Added
17
+
18
+ - Tested on Python 3.14. Every Python version CPython still supports is supported, and a version is
19
+ dropped in the first minor release after its end of life (Python 3.10's is 2026-10-31).
20
+ - A release procedure for maintainers in CONTRIBUTING.
21
+
22
+ ### Removed
23
+
24
+ - `RetrievalEvent.extra`, a field nothing ever set. Audit lines no longer carry an empty `"extra"`.
25
+ Removed before 1.0 so that it is not frozen into the stable API; adding a field later is
26
+ compatible, removing one is not.
27
+
28
+ ## [1.0.0rc1] - 2026-10-16
29
+
30
+ The first release candidate for 1.0. The public API in the
31
+ [API reference](https://muzakkirhussain011.github.io/needtoknow/api.html) is what 1.0.0 will ship:
32
+ changes before then are fixes, unless testing the release candidate shows that a breaking change is
33
+ needed, in which case it will be announced in a second release candidate. Install it with
34
+ `pip install --pre needtoknow`.
35
+
36
+ ### Added
37
+
38
+ - `PermissionedRetriever` with `prefilter`, `postfilter` and `auto` strategies, sync
39
+ (`retrieve`) and async (`aretrieve`) entry points sharing one implementation.
40
+ - Failing closed: any authorizer error, wrong number of decisions or non-boolean decision raises
41
+ `AuthorizationError` and returns nothing.
42
+ - Re-check of pre-filtered results, with `RetrievalResult.violations` counting hits that a
43
+ backend returned despite the filter.
44
+ - A backend-neutral filter language (`In`, `And`, `Or`) with reference semantics in `matches`;
45
+ empty `And`/`Or` are rejected because an empty conjunction would mean "allow everything".
46
+ - `MetadataACLAuthorizer` (permissions stored on each chunk) and `FunctionAuthorizer` (your own
47
+ logic).
48
+ - `InMemoryBackend` with deterministic keyword scoring, for tests and small corpora.
49
+ - Property-based tests (Hypothesis) for the security guarantees on Python 3.10 to 3.13.
50
+ - Contributor documentation: CONTRIBUTING, GOVERNANCE, CODE_OF_CONDUCT, SECURITY.
51
+ - `needtoknow.backends.pgvector`: PostgreSQL + pgvector backend with the permission filter in the
52
+ query as JSON containment tests (GIN-indexable), iterative HNSW scans, `create_schema`, and a
53
+ native async search when given an async pool. Extra: `needtoknow[pgvector]`.
54
+ - `needtoknow.backends.chroma`: Chroma backend translating filters into `where` clauses. It never
55
+ stores Chroma array metadata: in chromadb 1.5.x (local mode) a deleted collection's array
56
+ metadata surfaces on new records in other collections and is used by `where` filters
57
+ (chroma-core/chroma#7594). List values become
58
+ one boolean field each, and search results are rebuilt from an exact JSON copy of the metadata,
59
+ so the re-check always sees the true permissions. Extra: `needtoknow[chroma]`.
60
+ - `needtoknow.authorizers.opa`: Open Policy Agent authorizer using only the standard library, with
61
+ per-chunk decisions and optional policy-provided filters; every failure fails closed. Example
62
+ policy in `examples/opa`.
63
+ - `filter_to_dict` / `filter_from_dict` for a JSON form of filters.
64
+ - `needtoknow.authorizers.openfga`: OpenFGA authorizer over the HTTP API (no extra dependency):
65
+ batched BatchCheck, ListObjects pre-filtering that falls back to post-filtering when the list may
66
+ be truncated, and the subject's groups sent as contextual tuples. Tested against a real OpenFGA
67
+ for random relationship graphs.
68
+ - `needtoknow.langchain`: `NeedToKnowRetriever` (a LangChain retriever; refuses to run without a
69
+ subject) and `LangChainVectorStoreBackend` (any LangChain vector store, post-filtered).
70
+ Extra: `needtoknow[langchain]`.
71
+ - `needtoknow.llamaindex`: `NeedToKnowRetriever`, `PermissionNodePostprocessor` and
72
+ `LlamaIndexBackend`. Extra: `needtoknow[llamaindex]`.
73
+ - Runnable examples for LangChain and LlamaIndex, executed in CI.
74
+ - `CachingAuthorizer`: caches pre-filters and per-chunk decisions (keyed by chunk id and
75
+ metadata) per subject for `ttl` seconds, with that as the documented revocation bound; errors and
76
+ malformed answers are never cached; `invalidate()`; sync and async.
77
+ - Retrieval events: every call, including failed ones, produces a `RetrievalEvent` for
78
+ `observers` (subject id, strategy, returned ids, denied and violation counts, duration, error
79
+ type, optional HMAC of the query). Never query or chunk text. `JsonLinesAudit` writes them as
80
+ JSON lines; observer failures are logged and never affect retrieval.
81
+ - `needtoknow.otel.OpenTelemetryObserver`: one span per retrieval with counts and outcomes, the
82
+ subject id only on request. Extra: `needtoknow[otel]`.
83
+ - Documentation site (Sphinx, MyST, Furo) in `docs/`: getting started, concepts, a security model,
84
+ operations, one page per integration, extending needtoknow, benchmarks and an API reference.
85
+ CI builds it with warnings as errors, and every push to `main` publishes it to
86
+ <https://muzakkirhussain011.github.io/needtoknow/>. Extra: `needtoknow[docs]`.
87
+ - Benchmark harness (`benchmarks/`): recall@k against exact ground truth, leaks, completeness and
88
+ latency for exact search, Chroma and pgvector across permission selectivity and layouts, with a
89
+ Kaggle notebook for real embeddings. CI runs a small version against real Chroma and PostgreSQL
90
+ and fails on any leak.
91
+ - Conformance tests: every backend must return exactly the documents the reference semantics
92
+ select, for random metadata and nested filters, and never leak through the retriever. CI runs
93
+ them against real PostgreSQL, Chroma and OPA.
94
+
95
+ ### Changed
96
+
97
+ - `RetrievalResult.complete` is documented precisely: it is `False` when needtoknow knows results
98
+ may be missing, and it trusts the backend's ranking, so it cannot see results an approximate index
99
+ failed to return. The benchmarks measure how often that happens.
100
+ - Filter values must be strings or integers, and metadata values of other types (booleans,
101
+ floats) never match. This keeps every backend's answer identical to the reference.
102
+ - The project is now `needtoknow`, a permission-aware retrieval library (ADR 0005). The earlier
103
+ Rampart gateway lives on as an example deployment in `examples/gateway`.
104
+
105
+ [Unreleased]: https://github.com/muzakkirhussain011/needtoknow/compare/v1.0.0...HEAD
106
+ [1.0.0]: https://github.com/muzakkirhussain011/needtoknow/compare/v1.0.0rc1...v1.0.0
107
+ [1.0.0rc1]: https://github.com/muzakkirhussain011/needtoknow/releases/tag/v1.0.0rc1
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Syed Muzakkir Hussain
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,297 @@
1
+ Metadata-Version: 2.5
2
+ Name: needtoknow
3
+ Version: 1.0.0
4
+ Summary: Permission-aware retrieval for RAG: every user gets only the chunks they are allowed to read.
5
+ Project-URL: Homepage, https://github.com/muzakkirhussain011/needtoknow
6
+ Project-URL: Repository, https://github.com/muzakkirhussain011/needtoknow
7
+ Project-URL: Issues, https://github.com/muzakkirhussain011/needtoknow/issues
8
+ Project-URL: Changelog, https://github.com/muzakkirhussain011/needtoknow/blob/main/CHANGELOG.md
9
+ Project-URL: Documentation, https://muzakkirhussain011.github.io/needtoknow/
10
+ Author: Syed Muzakkir Hussain
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: access-control,authorization,langchain,llamaindex,llm,openfga,rag,retrieval,security,vector-search
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
25
+ Classifier: Topic :: Security
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.10
28
+ Provides-Extra: chroma
29
+ Requires-Dist: chromadb>=1.5; extra == 'chroma'
30
+ Provides-Extra: dev
31
+ Requires-Dist: hypothesis>=6.100; extra == 'dev'
32
+ Requires-Dist: pytest>=8.2; extra == 'dev'
33
+ Requires-Dist: ruff==0.17.1; extra == 'dev'
34
+ Provides-Extra: docs
35
+ Requires-Dist: furo>=2025.12.19; extra == 'docs'
36
+ Requires-Dist: myst-parser>=5.1; extra == 'docs'
37
+ Requires-Dist: sphinx>=9.1; extra == 'docs'
38
+ Provides-Extra: langchain
39
+ Requires-Dist: langchain-core>=1.0; extra == 'langchain'
40
+ Provides-Extra: llamaindex
41
+ Requires-Dist: llama-index-core>=0.12; extra == 'llamaindex'
42
+ Provides-Extra: otel
43
+ Requires-Dist: opentelemetry-api>=1.30; extra == 'otel'
44
+ Provides-Extra: pgvector
45
+ Requires-Dist: psycopg[binary,pool]>=3.2; extra == 'pgvector'
46
+ Description-Content-Type: text/markdown
47
+
48
+ # needtoknow
49
+
50
+ [![CI](https://github.com/muzakkirhussain011/needtoknow/actions/workflows/ci.yml/badge.svg)](https://github.com/muzakkirhussain011/needtoknow/actions/workflows/ci.yml)
51
+ [![Docs](https://github.com/muzakkirhussain011/needtoknow/actions/workflows/docs.yml/badge.svg)](https://muzakkirhussain011.github.io/needtoknow/)
52
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/muzakkirhussain011/needtoknow/blob/main/LICENSE)
53
+ ![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)
54
+ ![Dependencies: none](https://img.shields.io/badge/core%20dependencies-none-brightgreen.svg)
55
+
56
+ **Permission-aware retrieval for RAG.** Every user gets only the chunks they are allowed to read,
57
+ whatever vector store, permission system or framework you use.
58
+
59
+ ```python
60
+ from needtoknow import PermissionedRetriever, MetadataACLAuthorizer, Subject
61
+
62
+ retriever = PermissionedRetriever(backend, MetadataACLAuthorizer())
63
+ result = retriever.retrieve("What are this year's salary bands?", Subject("bob", groups=["engineering"]), k=5)
64
+ # result.hits holds only chunks Bob may read. HR documents never reach the prompt.
65
+ ```
66
+
67
+ **[Documentation](https://muzakkirhussain011.github.io/needtoknow/)** ·
68
+ [Security model](https://muzakkirhussain011.github.io/needtoknow/security.html) ·
69
+ [Benchmarks](https://muzakkirhussain011.github.io/needtoknow/benchmarks.html) ·
70
+ [Changelog](https://github.com/muzakkirhussain011/needtoknow/blob/main/CHANGELOG.md)
71
+
72
+ ## Why
73
+
74
+ A RAG assistant over company documents is only as safe as its retrieval step. If the vector search
75
+ ignores who is asking, an engineer can ask the assistant about salaries and get HR documents
76
+ summarised back. Every team building internal RAG has to solve this, and most solve it by hand,
77
+ differently each time.
78
+
79
+ `needtoknow` is that layer as a small, tested library:
80
+
81
+ - **Works with what you have.** Wrap any search backend and any permission system. The core has
82
+ no dependencies.
83
+ - **Filters in the right place.** When the permission system can describe "everything Bob may
84
+ read" as a filter, the filter goes *inside* the vector search, so results are the true top-k of
85
+ what Bob may read. When it cannot, candidates are checked after the search and the search widens
86
+ until enough permitted results are found.
87
+ - **Fails closed.** If the permission service errors, times out or returns malformed answers, the
88
+ user gets an error, never unfiltered results.
89
+ - **Defence in depth.** Pre-filtered results are checked again, so a backend bug that ignores the
90
+ filter cannot leak. Each result reports how many such violations it caught.
91
+
92
+ ## Install
93
+
94
+ ```bash
95
+ pip install needtoknow
96
+ ```
97
+
98
+ Python 3.10 or newer: every Python version CPython still supports, each dropped in the first minor
99
+ release after its end of life. Adapters for external systems install as optional extras.
100
+
101
+ ## Quickstart
102
+
103
+ ```python
104
+ from needtoknow import Document, InMemoryBackend, MetadataACLAuthorizer, PermissionedRetriever, Subject
105
+
106
+ backend = InMemoryBackend(
107
+ [
108
+ Document("handbook", "Annual leave is 25 days.", {"allowed_groups": ["*"]}),
109
+ Document("salaries", "Salary bands for 2026 ...", {"allowed_groups": ["hr"]}),
110
+ Document("runbook", "Roll back a failed deploy ...", {"allowed_groups": ["engineering"]}),
111
+ ]
112
+ )
113
+ retriever = PermissionedRetriever(backend, MetadataACLAuthorizer())
114
+
115
+ result = retriever.retrieve("leave salary bands", Subject("bob", groups=["engineering"]), k=3)
116
+ print([hit.id for hit in result.hits]) # ['handbook']
117
+ print(result.strategy, result.complete) # prefilter True
118
+ ```
119
+
120
+ `aretrieve` is the async equivalent, with the same arguments and guarantees.
121
+
122
+ ## How it works
123
+
124
+ | Strategy | When | Cost | Result |
125
+ |---|---|---|---|
126
+ | `prefilter` | The authorizer can return a filter and the backend supports filters | One search | The exact top-k of what the subject may read |
127
+ | `postfilter` | Any authorizer | Searches `k × overfetch` candidates, widening up to `max_candidates` | Exact when enough permitted candidates exist; `complete=False` otherwise |
128
+ | `auto` (default) | Picks `prefilter` when both sides support it | | |
129
+
130
+ Every call returns a `RetrievalResult` with the hits plus `strategy`, `examined`, `denied`,
131
+ `violations` (pre-filtered hits the re-check rejected, which should always be 0) and `complete`.
132
+
133
+ ## Integrations
134
+
135
+ Each integration is an optional extra, so the core stays dependency-free.
136
+
137
+ **PostgreSQL + pgvector** (`pip install "needtoknow[pgvector]"`, pgvector 0.8+). The permission
138
+ filter runs inside the SQL query as JSON containment tests, served by a GIN index, and iterative
139
+ index scans keep HNSW returning k rows under selective filters.
140
+
141
+ ```python
142
+ from psycopg_pool import ConnectionPool
143
+
144
+ from needtoknow.backends.pgvector import PgVectorBackend, create_schema
145
+
146
+ pool = ConnectionPool("postgresql://localhost/app")
147
+ create_schema(pool, dim=768)
148
+ backend = PgVectorBackend(pool, embed=my_embedding_model) # embed: str -> list[float]
149
+ backend.add(documents)
150
+ ```
151
+
152
+ **Chroma** (`pip install "needtoknow[chroma]"`). The filter becomes Chroma's `where` clause. Each
153
+ value in a permission list is stored as its own field rather than as a Chroma array, and an exact
154
+ copy of the metadata is kept for the re-check: in chromadb 1.5.x, deleting a collection leaves its
155
+ array metadata behind for new records in other collections to pick up
156
+ ([chroma-core/chroma#7594](https://github.com/chroma-core/chroma/issues/7594)), which for
157
+ permissions would be a leak.
158
+
159
+ ```python
160
+ import chromadb
161
+
162
+ from needtoknow.backends.chroma import ChromaBackend
163
+
164
+ collection = chromadb.PersistentClient(path="./chroma").get_or_create_collection("docs")
165
+ backend = ChromaBackend(collection) # uses the collection's embedding function
166
+ ```
167
+
168
+ **Open Policy Agent** (no extra dependency). Your Rego policy returns one decision per chunk, and
169
+ can optionally return a filter so the vector store does the filtering. A complete policy is in
170
+ [`examples/opa/needtoknow.rego`](https://github.com/muzakkirhussain011/needtoknow/blob/main/examples/opa/needtoknow.rego).
171
+
172
+ ```python
173
+ from needtoknow.authorizers.opa import OPAAuthorizer
174
+
175
+ authorizer = OPAAuthorizer("http://localhost:8181", filter_path="needtoknow/filter")
176
+ ```
177
+
178
+ **OpenFGA** (no extra dependency). Relationship-based access control in the Google Zanzibar style.
179
+ Checks run in batches; for pre-filtering, ListObjects returns every document the user may view,
180
+ and if that list may have been truncated the retriever post-filters instead. The subject's groups
181
+ can be sent as contextual tuples, so memberships from your identity provider need no syncing.
182
+
183
+ ```python
184
+ from needtoknow.authorizers.openfga import OpenFGAAuthorizer
185
+
186
+ authorizer = OpenFGAAuthorizer("http://localhost:8080", store_id, resource_field="doc_id")
187
+ ```
188
+
189
+ **LangChain** (`pip install "needtoknow[langchain]"`). `NeedToKnowRetriever` is a LangChain
190
+ retriever, so it works in chains, agents and LangGraph tools; `LangChainVectorStoreBackend` turns
191
+ any LangChain vector store into a needtoknow backend (post-filtered).
192
+
193
+ ```python
194
+ from needtoknow.langchain import NeedToKnowRetriever
195
+
196
+ retriever = NeedToKnowRetriever(retriever=PermissionedRetriever(backend, authorizer))
197
+ docs = retriever.invoke("What are this year's salary bands?", subject=Subject("bob", ["sales"]))
198
+ ```
199
+
200
+ **LlamaIndex** (`pip install "needtoknow[llamaindex]"`). `NeedToKnowRetriever` for query and chat
201
+ engines, `PermissionNodePostprocessor` for existing pipelines, and `LlamaIndexBackend` to wrap an
202
+ existing index. Runnable examples for both frameworks are in [`examples/`](https://github.com/muzakkirhussain011/needtoknow/tree/main/examples).
203
+
204
+ Filter values are strings or integers, the types permission identifiers use. That keeps every
205
+ backend's answer identical to the reference semantics, which the test suite checks for each one.
206
+
207
+ ## Caching, audit and tracing
208
+
209
+ ```python
210
+ from needtoknow import CachingAuthorizer, JsonLinesAudit
211
+ from needtoknow.otel import OpenTelemetryObserver # pip install "needtoknow[otel]"
212
+
213
+ retriever = PermissionedRetriever(
214
+ backend,
215
+ CachingAuthorizer(authorizer, ttl=30), # a revoked permission stops granting access within 30 s
216
+ observers=[JsonLinesAudit(audit_file), OpenTelemetryObserver()], # audit_file: an open text file
217
+ audit_key=b"...", # optional: an HMAC of each query, so repeats can be matched without storing them
218
+ )
219
+ ```
220
+
221
+ `CachingAuthorizer` caches pre-filters and per-chunk decisions per subject for `ttl` seconds and
222
+ never caches an error; `invalidate()` drops decisions after a change you know about. Every
223
+ retrieval, including failed ones, produces one `RetrievalEvent` (who asked, strategy, returned ids,
224
+ denied and violation counts, duration, error type). Events never contain query or chunk text, and
225
+ an observer that fails is logged without affecting retrieval.
226
+
227
+ ## Bring your own backend or permission system
228
+
229
+ A backend is any object with a `search` method:
230
+
231
+ ```python
232
+ class MyBackend:
233
+ supports_filters = True # set False if search() ignores `filter`
234
+
235
+ def search(self, query: str, k: int, filter=None) -> list[Hit]:
236
+ """Return hits best first. Translate `filter` (In / And / Or) into your store's filter."""
237
+ ```
238
+
239
+ An authorizer answers two questions:
240
+
241
+ ```python
242
+ class MyAuthorizer:
243
+ def check(self, subject, hits, action="read") -> list[bool]:
244
+ """One decision per hit, in order. Raise on any doubt: the retriever fails closed."""
245
+
246
+ def filter_for(self, subject, action="read"):
247
+ """A Filter selecting everything the subject may read, or None if you cannot express one."""
248
+ ```
249
+
250
+ Either can add `async` versions (`asearch`, `acheck`, `afilter_for`); otherwise the sync methods
251
+ run in a thread under `aretrieve`. `FunctionAuthorizer` wraps a plain function.
252
+
253
+ ## Testing approach
254
+
255
+ The guarantees are tested as properties with [Hypothesis](https://hypothesis.readthedocs.io):
256
+ for randomly generated corpora, permissions, users and queries, every strategy must
257
+
258
+ 1. never return a chunk the user may not read,
259
+ 2. return exactly the top-k of what the user may read (checked against an independent oracle),
260
+ 3. stay safe when the backend ignores the filter, and
261
+ 4. give identical results through `retrieve` and `aretrieve`.
262
+
263
+ ## Benchmarks
264
+
265
+ On 57,600 real chunks (BEIR FiQA, Qwen3-Embedding-0.6B), across 33,000 retrievals, no needtoknow
266
+ strategy returned a single forbidden chunk. Filtering inside the search kept recall@10 at 0.949 or
267
+ better on pgvector for users who may read anywhere from 0.06% to 100% of the corpus, including when
268
+ what they may read is far from what they ask about. Filtering after a top-10 search found almost
269
+ nothing for selective permissions. Method, tables and caveats: [Benchmarks](https://muzakkirhussain011.github.io/needtoknow/benchmarks.html).
270
+
271
+ | Readable (far from the query) | Naive top-k, then filter | needtoknow postfilter | needtoknow prefilter (pgvector) |
272
+ |---|---|---|---|
273
+ | 0.9% | 0.000 | 0.048 | 0.995 |
274
+ | 4.8% | 0.000 | 0.314 | 0.950 |
275
+ | 20% | 0.041 | 0.974 | 0.949 |
276
+
277
+ ## Stability and roadmap
278
+
279
+ 1.0 is the stable base: the core, the pgvector and Chroma backends, the OPA and OpenFGA authorizers,
280
+ the LangChain and LlamaIndex integrations, caching, audit events and tracing. The public API in the
281
+ [API reference](https://muzakkirhussain011.github.io/needtoknow/api.html) follows semantic
282
+ versioning: no breaking change without a major version and a deprecation period first.
283
+
284
+ Next, in rough order: permission sync from Google Drive, SharePoint and Confluence; Qdrant and
285
+ Elasticsearch backends; SpiceDB and Cerbos authorizers. Contributions are welcome; see
286
+ [CONTRIBUTING.md](https://github.com/muzakkirhussain011/needtoknow/blob/main/CONTRIBUTING.md).
287
+
288
+ ## Example deployment
289
+
290
+ [`examples/gateway`](https://github.com/muzakkirhussain011/needtoknow/tree/main/examples/gateway) is a complete, self-hosted, OpenAI-compatible gateway built
291
+ on the same ideas: OPA policies, pgvector hybrid search with the ACL inside the query, PII
292
+ pseudonymisation, prompt-injection quarantine and a hash-chained audit log. It is being ported onto
293
+ this library.
294
+
295
+ ## License
296
+
297
+ [MIT](https://github.com/muzakkirhussain011/needtoknow/blob/main/LICENSE)