Skip to content

feat(tenancy): logical multi-tenancy — per-tenant config resolution (Step 6.1) - #148

Merged
officialCodeWork merged 1 commit into
mainfrom
build/phase-6/step-6.1-logical-multitenancy
Jun 7, 2026
Merged

feat(tenancy): logical multi-tenancy — per-tenant config resolution (Step 6.1)#148
officialCodeWork merged 1 commit into
mainfrom
build/phase-6/step-6.1-logical-multitenancy

Conversation

@officialCodeWork

Copy link
Copy Markdown
Owner

Summary

Step 6.1 — Logical multi-tenancy: make per-tenant rag.yaml config actually drive each request, and start Phase 6 (Governance & Tenancy). Built as a single PR.

Before this, TenantConfig was 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 → frozen TenantSettings (rag_core.types: tenant_id / namespace / pii_policy / acl_labels), merging the tenants: entry with safe defaults.
  • Applied once at the gateway boundary (build_gateway_context): the resolved namespace + pii_policy land on the RequestContext, and the tenant's acl_labels are unioned into the principal's. The resolved view is also exposed on request.state.gateway.tenant_settings.
  • Unknown tenants resolve to safe defaults (namespace = the id, default PII redact, no labels) — isolated, never privileged.
  • New RequestContext.namespace defaults to tenant_id via a before-validator, so every pre-6.1 call site is unchanged. It's a backend-partition primitive — Pinecone now partitions on ctx.namespacenot a chunk field, so PolicyEngine.filter_pushdown is unchanged (chunks carry tenant_id).
  • TenantConfig gains namespace + acl_labels; GET /v1/status/tenant surfaces the resolved view; ragctl tenant list / resolve <id> print it (the scaffold is promoted to a real command).
  • Inert by default in build_app; build_app_from_config builds the resolver from cfg.tenants.

TenantSettings joins dist/schemas, RequestContext regenerated, TenantConfig fields → rag.schema, /v1/status/tenantdist/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

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 1152 passed (one transient config hot-reload-watcher flake, unrelated; reran clean)
  • ~21 new tests: TenantResolver unit (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.yaml ran end-to-end (known + unknown tenants)

🤖 Generated with Claude Code

…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>
@officialCodeWork
officialCodeWork merged commit 7a7aefc 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