Skip to content

feat(tenancy): physical tenancy — dedicated index per tenant (Step 6.2) - #149

Merged
officialCodeWork merged 1 commit into
mainfrom
build/phase-6/step-6.2-physical-tenancy
Jun 7, 2026
Merged

feat(tenancy): physical tenancy — dedicated index per tenant (Step 6.2)#149
officialCodeWork merged 1 commit into
mainfrom
build/phase-6/step-6.2-physical-tenancy

Conversation

@officialCodeWork

Copy link
Copy Markdown
Owner

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_id filter + namespace), 6.2 is physical (a dedicated index per tenant), opt-in per tenant.

  • Config: TenantConfig.dedicated_index: bool (+ optional dedicated_index_name). 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 — the same resolve-once-at-the-boundary pattern as 6.1.
  • Backends read only ctx (the dependency 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), and Qdrant (dedicated collection). pgvector / Elasticsearch / Weaviate follow the same pattern later.
  • 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-service isolation is covered by the Pinecone / Qdrant integration tests.
  • One backend instance + per-tenant derivation — no per-tenant backend instances, no SPI change (the methods already take ctx).
  • GET /v1/status/tenant + ragctl tenant resolve <id> report dedicated_index / physical_index.

TenantConfig fields → rag.schema; TenantSettings + RequestContext regenerated; /v1/status/tenantdist/openapi.

Key decisions (see ADR-0034)

  • The per-tenant index decision reaches the backend via ctx.physical_index (not a resolver call), because rag-backends may import rag-core only.
  • Uniform <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

Test plan

  • ruff check + ruff format --check clean
  • mypy --strict clean (293 source files)
  • RAG001 logging gate clean
  • schema-drift + openapi-drift regenerated (committed); proto-compat + policy-coverage pass
  • full suite 2529 passed, 4 skipped (stable order)
  • ~30 new/updated tests, incl. the cross-tenant dedicated-index probe gate (physical isolation independent of the tenant filter), resolver/config (tests/config/test_tenancy.py), /v1/status/tenant dedicated-index reporting (apps/gateway/tests/test_tenancy.py), and ragctl tenant (packages/ragctl/tests/test_tenant.py)
  • ragctl tenant resolve acme -f rag.yaml shows index: dedicated (key=acme) end-to-end

🤖 Generated with Claude Code

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>
@officialCodeWork
officialCodeWork merged commit 2d9319e into main Jun 7, 2026
12 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant