feat(tenancy): logical multi-tenancy — per-tenant config resolution (Step 6.1) - #148
Merged
officialCodeWork merged 1 commit intoJun 7, 2026
Merged
Conversation
…Step 6.1) Make per-tenant rag.yaml config actually drive requests. Before this it was a passive registry (only quotas were per-tenant); now a TenantResolver (rag_config.tenancy) maps a tenant id to a frozen TenantSettings (rag_core.types: namespace / pii_policy / acl_labels), merging the tenants: entry with safe defaults, and the gateway applies it once at the request boundary (build_gateway_context): namespace + pii_policy land on the RequestContext and the tenant's acl_labels are unioned into the principal's. - RequestContext gains a namespace field that defaults to tenant_id (a before-validator), so every pre-6.1 call site is unchanged. It's a backend-partition primitive — Pinecone now partitions on ctx.namespace — not a chunk field, so PolicyEngine.filter_pushdown is unchanged. - Unknown tenants resolve to safe defaults (namespace = id, default PII redact, no labels): isolated, never privileged. - TenantConfig gains namespace + acl_labels (-> rag.schema); TenantSettings joins dist/schemas and RequestContext regenerated; GET /v1/status/tenant (-> openapi) surfaces the resolved view; ragctl tenant list / resolve print it. - Inert by default in build_app; build_app_from_config builds the resolver from cfg.tenants. Scope is resolution + threading only: ACL push-down enforcement is 6.3, PII egress is 6.5, physical tenancy is 6.2. Starts Phase 6; ~21 new tests (resolver, config, gateway, cross-tenant red-team, CLI). 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.1 — Logical multi-tenancy: make per-tenant
rag.yamlconfig actually drive each request, and start Phase 6 (Governance & Tenancy). Built as a single PR.Before this,
TenantConfigwas a passive registry — the gateway used one global config for every tenant, and only quotas (Step 4.5) were per-tenant. Now:TenantResolver(rag_config.tenancy) maps a tenant id → frozenTenantSettings(rag_core.types:tenant_id/namespace/pii_policy/acl_labels), merging thetenants:entry with safe defaults.build_gateway_context): the resolvednamespace+pii_policyland on theRequestContext, and the tenant'sacl_labelsare unioned into the principal's. The resolved view is also exposed onrequest.state.gateway.tenant_settings.redact, no labels) — isolated, never privileged.RequestContext.namespacedefaults totenant_idvia a before-validator, so every pre-6.1 call site is unchanged. It's a backend-partition primitive — Pinecone now partitions onctx.namespace— not a chunk field, soPolicyEngine.filter_pushdownis unchanged (chunks carrytenant_id).TenantConfiggainsnamespace+acl_labels;GET /v1/status/tenantsurfaces the resolved view;ragctl tenant list/resolve <id>print it (the scaffold is promoted to a real command).build_app;build_app_from_configbuilds the resolver fromcfg.tenants.TenantSettingsjoinsdist/schemas,RequestContextregenerated,TenantConfigfields →rag.schema,/v1/status/tenant→dist/openapi.Scope & boundaries
This is the logical-tenancy foundation — resolution + threading of the per-tenant isolation/governance primitives. The enforcement that builds on them lands later in Phase 6: ACL push-down at retrieval is 6.3, PII egress enforcement is 6.5, physical tenancy (dedicated index) is 6.2.
Documentation
namespacefield + resolver noteTest plan
ruff check+ruff format --checkcleanmypy --strictclean (293 source files)TenantResolverunit (tests/config/test_tenancy.py), gateway boundary +/v1/status/tenant(apps/gateway/tests/test_tenancy.py), cross-tenant red-team namespace/ACL/PII isolation (tests/redteam/test_cross_tenant_namespace.py),ragctl tenant(packages/ragctl/tests/test_tenant.py)ragctl tenant resolve acme -f rag.yamlran end-to-end (known + unknown tenants)🤖 Generated with Claude Code