Skip to content

feat: opt-in tenant isolation across all six blocks - #58

Merged
mkorbi merged 2 commits into
mainfrom
feat/tenant-isolation
Jul 28, 2026
Merged

feat: opt-in tenant isolation across all six blocks#58
mkorbi merged 2 commits into
mainfrom
feat/tenant-isolation

Conversation

@mkorbi

@mkorbi mkorbi commented Jul 25, 2026

Copy link
Copy Markdown
Member

Opt-in tenant_isolation config flag (default off = byte-identical to before). Two mechanisms:

  • kv / episodic / lease / graph / artifact — service-layer namespace qualification via auth.QualifyNamespace(tenant, ns, isolate) (tenant \x1f ns); centrally enforced, zero driver/schema change.
  • semantic — driver-level tenant column: composite (tenant, id) PK, tenant-scoped queries, idempotent PK-upgrade DO block.

Non-breaking (default-off, existing data unaffected). Verified: kv 2-tenant service test; full semantic conformance on real pgvector incl. a new TenantIsolation case; PK-upgrade path verified via psql.

🤖 Generated with Claude Code

mkorbi and others added 2 commits July 23, 2026 12:11
Closes the multi-tenant storage-isolation gap: a namespace is a physical store,
so two tenants both scoped to the same namespace name previously shared data.
With `tenant_isolation: true` each request's storage namespace is qualified with
the caller's capability tenant, so tenants sharing a namespace name get
physically separate data.

Enforcement lives in ONE place — the service layer, via
auth.QualifyNamespace(tenant, namespace, isolate), before the driver is ever
touched — so a driver can't leak across tenants even by accident, and no driver
or schema changes were needed for these five blocks.

- Off by default: storage is byte-for-byte identical to before, so single-tenant
  deployments and existing data are unaffected. Opt in via `tenant_isolation`.
- Each block service gained the flag (threaded from config → server.Deps); its
  resolve helper now returns the qualified storage namespace and every driver
  data call uses it, while auth checks + registry resolution keep using the
  config namespace.
- lease no longer echoes the (qualified) storage namespace back to clients — the
  wire handle carries the config namespace, so the internal prefix never leaks.
- semantic is bound per-namespace and needs a driver-level tenant column (not yet
  implemented), so config validation refuses tenant_isolation + a semantic
  namespace rather than silently sharing semantic data.

Verified: a two-tenant kv service test (isolated get/scan/delete when on; shared
last-write-wins when off), config guard tests, full go test -race, golangci-lint,
and the docs build all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes tenant isolation across all six blocks. Semantic is bound per-namespace
(the driver has no namespace parameter to qualify), so instead of the
service-layer namespace prefix the five other blocks use, it carries a tenant
dimension in the driver: (tenant, id) is a record's identity, and every read and
write is tenant-scoped. Two tenants can reuse the same record id without
colliding. Removes the startup guard that previously refused isolation + semantic.

- driver.go: Tenant on Record + SearchOptions/DeleteOptions/ExpireOptions.
- service.go: threads auth.StorageTenant(cap.Tenant, isolation) — set on each
  upserted record and passed to search/delete/expire.
- memory driver: records keyed by the composite (tenant, id); Search/Delete/
  Expire/supersede all scope to the tenant.
- postgres driver: a `tenant text NOT NULL DEFAULT ''` column and a composite
  (tenant, id) primary key; every dense/sparse/hybrid search (via the shared
  filterConds), materialize, upsert (incl. CAS + supersede), delete, and expire
  is tenant-scoped. ensureSchema adds the column and idempotently upgrades a
  pre-isolation (id)-only primary key to (tenant, id); existing rows land under
  tenant='' (the un-isolated partition, matching pre-isolation reads).
- New conformance case (both harnesses) proves two tenants sharing an id are
  isolated on search and delete.

Verified: memory conformance, the full semantic conformance on real pgvector
(all existing cases + the new isolation case), and the idempotent PK-upgrade
path (old id-only PK → (tenant, id), re-runnable, old row preserved). go build/
vet, go test -race, golangci-lint, and the docs build all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mkorbi
mkorbi merged commit 711bdcd into main Jul 28, 2026
3 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