feat(tenancy): physical tenancy — dedicated index per tenant (Step 6.2) - #149
Merged
Merged
Conversation
Give a tenant its own vector index/collection, so its data is physically separate even if a filter were bypassed — for tenants that require hard isolation. Opt-in per tenant via TenantConfig.dedicated_index (+ optional dedicated_index_name). Same resolve-once-at-the-boundary pattern as 6.1: the resolver computes a physical_index key (dedicated_index_name or tenant_id, else None) on TenantSettings, threaded onto the new RequestContext.physical_index by the gateway middleware. Backends read only ctx (the graph is backends->core, never rag-config), namespace their base index under the key (<base>-<key>), and lazily create the dedicated index/collection on first use; None keeps the shared base index. Implemented for Noop (the CI conformance oracle, partitioned by physical_index) + Pinecone (dedicated index) + Qdrant (dedicated collection); pgvector/ES/Weaviate follow the same pattern later. A cross-tenant probe gate (tests/redteam/test_cross_tenant_dedicated_index.py, CI, Noop) proves a dedicated tenant's data is invisible to another *independent of the tenant filter* (a matching tenant_id pointed at a different index still returns nothing); live backends are integration-tested. One backend instance + per-tenant derivation (no per-tenant instances, no SPI change). GET /v1/status/tenant + ragctl tenant resolve report dedicated_index / physical_index. TenantConfig fields -> rag.schema; TenantSettings + RequestContext regenerated; /v1/status/tenant -> openapi. ~30 new/updated tests; all gates green (ruff, mypy --strict 293 files, RAG001, schema/openapi-drift, proto-compat, policy-coverage). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Step 6.2 — Physical tenancy: give a tenant its own vector index/collection, so its data is physically separate even if a filter were bypassed — for tenants that require hard isolation. Built as a single PR.
Where 6.1 was logical (one shared index +
tenant_idfilter + namespace), 6.2 is physical (a dedicated index per tenant), opt-in per tenant.TenantConfig.dedicated_index: bool(+ optionaldedicated_index_name). The resolver computes aphysical_indexkey (dedicated_index_name or tenant_id, elseNone) onTenantSettings, threaded onto the newRequestContext.physical_indexby the gateway middleware — the same resolve-once-at-the-boundary pattern as 6.1.ctx(the dependency graph is backends→core, neverrag-config), namespace their base index under the key (<base>-<key>), and lazily create the dedicated index/collection on first use;Nonekeeps the shared base index. Implemented for Noop (the CI conformance oracle, partitioned byphysical_index), Pinecone (dedicated index), and Qdrant (dedicated collection). pgvector / Elasticsearch / Weaviate follow the same pattern later.tests/redteam/test_cross_tenant_dedicated_index.py, CI, Noop) proves a dedicated tenant's data is invisible to another independent of the tenant filter — a matchingtenant_idpointed at a different index still returns nothing. Live-service isolation is covered by the Pinecone / Qdrant integration tests.ctx).GET /v1/status/tenant+ragctl tenant resolve <id>reportdedicated_index/physical_index.TenantConfigfields →rag.schema;TenantSettings+RequestContextregenerated;/v1/status/tenant→dist/openapi.Key decisions (see ADR-0034)
ctx.physical_index(not a resolver call), becauserag-backendsmay importrag-coreonly.<base>-<key>naming (collision-safe under the deployment base); lazy check-and-create; Noop as the CI oracle so the probe gate runs without live services.Documentation
physical_index+ probe gatephysical_indexfieldTest plan
ruff check+ruff format --checkcleanmypy --strictclean (293 source files)tests/config/test_tenancy.py),/v1/status/tenantdedicated-index reporting (apps/gateway/tests/test_tenancy.py), andragctl tenant(packages/ragctl/tests/test_tenant.py)ragctl tenant resolve acme -f rag.yamlshowsindex: dedicated (key=acme)end-to-end🤖 Generated with Claude Code