chancel
Provable scope isolation for AI retrieval. Firm-level instructions travel everywhere; a client matter's data provably never crosses a matter boundary — and the boundary is enforced below the model boundary, so it survives swapping the AI provider.
Demo matrix — regenerated by CI on every push. The table below is the literal output of
chancel demo --no-llm. CI runs it on every push and fails the build if a single cell diverges
from what this repo claims. (The badges 404 until the repo is public; that is expected.)
The proof, in one table
finding backend provider expected actual ok
---------------------- -------- ------------ -------- ------ --
canary-leak isolated echo clean clean ✓
canary-leak isolated hostile_echo clean clean ✓
unrepresentable-call isolated n/a denied denied ✓
deletion-verifiability isolated n/a clean clean ✓
canary-leak filtered echo clean clean ✓
canary-leak filtered hostile_echo clean clean ✓
unrepresentable-call filtered n/a leaked leaked ✓
deletion-verifiability filtered n/a leaked leaked ✓
canary-leak shared echo leaked leaked ✓
canary-leak shared hostile_echo leaked leaked ✓
unrepresentable-call shared n/a leaked leaked ✓
deletion-verifiability shared n/a leaked leaked ✓
PASS: 12/12 cells matched their predicted color
This is the literal output of chancel demo --no-llm. It runs from a cold clone with no API
key and no Docker — the demo uses an in-memory store and a deterministic offline embedder.
"Green" does not mean "no leak." It means every cell behaved as predicted: isolated clean,
shared leaking as designed, filtered red exactly where it is supposed to be red. A ✗ would
be the alarm — a backend no longer behaving the way this repo says it does.
Quickstart
git clone https://github.com/Nobel-Co/chancel && cd chancel
uv sync
just demo # or: uv run chancel demo --no-llm
The three backends
Each backend implements the same logical vocabulary — one firm collection plus one collection per matter — and differs only in how that maps onto physical storage and whether the boundary is a structural fact or a runtime decision.
| backend | layout | what it is | the finding it must face |
|---|---|---|---|
isolated |
one physical collection per space + a firm collection | the claim being defended | holds all four findings — a cross-space read has no signature to express |
filtered |
one shared collection, space_id payload, must-filter every query |
the vendor-default multitenancy pattern | leaks the unrepresentable-call and deletion-verifiability findings; the sparse-IDF side channel shifts one matter's scores by another's vocabulary |
shared |
one shared collection, no filter, system prompt says "don't cross matters" | what much shipped software actually does | leaks everything, starting with a direct canary read |
The shared backend is deliberately weak and is asserted red in CI: fixing it would delete the
negative example the suite needs. See architecture.md for the full
module contract.
Why collection-per-space when the vendor recommends against it
The vector-store vendor's own guidance is to prefer a single shared collection over collection-per-tenant, and it is right — but that is a scaling recommendation, not a security one. Its exact words:
"Creating a separate collection for each tenant is rarely the most efficient approach… Only create multiple collections when you have a limited number of tenants that need strict isolation."
A law firm's matters are exactly that named exception: a bounded number of tenants that need
strict isolation, where the isolation benefit outweighs the operational cost of extra
collections. chancel occupies the exception on purpose. See
ADR 0003.
Fork in five minutes
The base install runs the unit and conformance suites green in under a minute, with no API
keys and no Docker — the reference inmemory store and hash_stub embedder carry the whole
default path.
uv sync
just test # unit + integration + conformance
just demo # the leak matrix above
Adding a provider or a store is implementing one protocol and passing tests/conformance/ —
there is a test that proves a fresh adapter drops in with zero edits to the suite. See
adding-a-provider.md and
adding-a-store.md.
This is a demonstration, not a product
chancel demonstrates exactly one idea end to end: a provider-neutral policy gate that makes a
cross-matter read structurally unrepresentable, with a red/green test suite proving it against
the two architectures the industry actually ships. It carries zero real data — the corpus is
synthetic and generated by a script.
Out of scope (named here so their omission is not mistaken for oversight): authentication, RBAC, multi-user sessions, key management, retention, egress DLP, answer-quality evaluation, any UI, and any hosted deployment.
Documentation
- Why — the migration argument, the memory tiers, and why the wall belongs below the provider boundary.
- Architecture — the request path, the three backends side by side, the memory-promotion gate, and the module-by-module contract.
- Threat model — what it defends, what it does not, and the trust boundaries.
- Architecture Decision Records — the name gate, the enforcement boundary, the vendor departure, and the audit hash chain.
MIT licensed. See LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file chancel-0.1.0.tar.gz.
File metadata
- Download URL: chancel-0.1.0.tar.gz
- Upload date:
- Size: 292.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
01058df7452f5f7a6d795a135d3f885cfb412f69f1e1418cef9e766ec18685be
|
|
| MD5 |
c378381b711c6e97221407be7c1b5fbb
|
|
| BLAKE2b-256 |
980353d7895d9c8fc8024496add0913d7929fae50dc914fd74a8f16ef21b88ed
|
Provenance
The following attestation bundles were made for chancel-0.1.0.tar.gz:
Publisher:
release.yml on Nobel-Co/chancel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
chancel-0.1.0.tar.gz -
Subject digest:
01058df7452f5f7a6d795a135d3f885cfb412f69f1e1418cef9e766ec18685be - Sigstore transparency entry: 2523943459
- Sigstore integration time:
-
Permalink:
Nobel-Co/chancel@365e38c0012b7804fb9965bef0c938d8b38c9f42 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Nobel-Co
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@365e38c0012b7804fb9965bef0c938d8b38c9f42 -
Trigger Event:
push
-
Statement type:
File details
Details for the file chancel-0.1.0-py3-none-any.whl.
File metadata
- Download URL: chancel-0.1.0-py3-none-any.whl
- Upload date:
- Size: 70.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d9b0606a8015c74a6a3d1f8b099ea8a82798da6f061a2d9781444d3c04070ad
|
|
| MD5 |
681848a67541c649d15f91347b59c3e1
|
|
| BLAKE2b-256 |
36d18a3a5ffb0d851060289a2ad82ef9c2940a5455e1b01b2ab5180c96ce5d43
|
Provenance
The following attestation bundles were made for chancel-0.1.0-py3-none-any.whl:
Publisher:
release.yml on Nobel-Co/chancel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
chancel-0.1.0-py3-none-any.whl -
Subject digest:
1d9b0606a8015c74a6a3d1f8b099ea8a82798da6f061a2d9781444d3c04070ad - Sigstore transparency entry: 2523943513
- Sigstore integration time:
-
Permalink:
Nobel-Co/chancel@365e38c0012b7804fb9965bef0c938d8b38c9f42 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Nobel-Co
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@365e38c0012b7804fb9965bef0c938d8b38c9f42 -
Trigger Event:
push
-
Statement type: