Skip to content

feat(vector_io): add tenant isolation for vector store metadata - #5782

Merged
leseb merged 12 commits into
ogx-ai:mainfrom
franciscojavierarceo:multi-tenancy/pr4-vector-store-isolation
May 12, 2026
Merged

feat(vector_io): add tenant isolation for vector store metadata#5782
leseb merged 12 commits into
ogx-ai:mainfrom
franciscojavierarceo:multi-tenancy/pr4-vector-store-isolation

Conversation

@franciscojavierarceo

@franciscojavierarceo franciscojavierarceo commented May 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Migrate OpenAIVectorStoreMixin metadata storage from KVStore to AuthorizedSqlStore for row-level tenant isolation across all 9 vector_io providers (faiss, sqlite-vec, chroma, qdrant, milvus, pgvector, weaviate, elasticsearch, infinispan)
  • Add vector_stores SqlStoreReference to ServerStoresConfig and metadata_store config field to each provider for SQL-backed metadata
  • Automatic KVStore-to-SQL data migration on first startup after upgrade (idempotent, works with SQLite and Postgres)
  • Integration tests for vector store tenant isolation using the Alice/Bob two-user pattern in the auth CI workflow
  • Maintain full backward compatibility: providers without metadata_store configured continue using KVStore unchanged

Context

This is PR4 in the multi-tenancy series:

Design

The OpenAIVectorStoreMixin stores 4 metadata domains (vector stores, files, file contents, file batches) that previously used KVStore with no access control. This PR adds a dual-path storage system:

  • If metadata_store (AuthorizedSqlStore) is configured → SQL tables with owner_principal and access_attributes columns for ABAC tenant isolation
  • If only kvstore is configured → existing KVStore behavior (backward compat)

Each provider factory function (get_provider_impl / get_adapter_impl) now accepts an optional policy parameter that the resolver auto-injects via signature introspection (same mechanism used for prompts and connectors in #5757).

Provider-specific storage (Faiss indices, Qdrant collections, pgvector tables, etc.) is unchanged — only the shared metadata layer is migrated.

KVStore→SQL Migration

When deploying with metadata_store configured for the first time, the server automatically migrates existing vector store data from KVStore to SQL during initialize():

  1. Checks if SQL tables are empty AND KVStore has data
  2. Copies all 4 domains: stores, files, file contents, batches
  3. Stamps migrated records with owner_principal="" (unowned) — accessible to all authenticated users, which is correct for pre-multi-tenancy data
  4. Uses only KVStore/SqlStore protocol interfaces — works with SQLite, Postgres, or any other backend
  5. Idempotent — skips if SQL tables already have data

Files Changed (42 files)

Core (3): datatypes.py, stack.py, storage/datatypes.py — add vector_stores SqlStoreReference
Mixin (1): openai_vector_store_mixin.py — dual-path SQL/KV metadata methods + KV→SQL migration
Inline providers (6): faiss + sqlite-vec {config, __init__, adapter}.py
Remote providers (21): 7 providers × {config, __init__, adapter}.py
Generated docs (9): provider doc pages with new metadata_store field
Integration tests (1): test_vector_stores_access_control.py — 5 tenant isolation tests
CI (1): integration-responses-conversations-auth-tests.yml — vector store ABAC policies + test execution

Test plan

  • Unit tests pass: uv run pytest tests/unit/ -x --tb=short (2277 passed)
  • Pre-commit hooks pass: mypy, ruff, ruff-format, provider codegen, all linters
  • Migration unit tests: 5 tests covering copy, skip-when-exists, skip-when-empty, files+chunks, batches
  • Integration tests: 5 tenant isolation tests (retrieve, delete, update, listing, access-after-denial)
  • CI auth workflow with minikube k8s tokens

🤖 Generated with Claude Code

…uthorizedSqlStore

Migrate OpenAIVectorStoreMixin metadata storage from KVStore to
AuthorizedSqlStore for row-level tenant isolation. All 9 vector_io
providers (2 inline, 7 remote) now accept a policy parameter and
optional metadata_store config for SQL-backed, access-controlled
metadata. Dual storage support preserves backward compatibility:
providers without metadata_store configured continue using KVStore.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>
franciscojavierarceo and others added 5 commits May 11, 2026 09:15
Add access control tests verifying that vector stores created by one
user are invisible to other users. Tests cover retrieve, update, delete,
and listing isolation using the Alice/Bob two-user pattern. Update the
auth CI workflow to configure metadata_store on faiss, add vector_stores
to server stores, and add ABAC access policies for all 4 metadata tables.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>
…ore metadata

When a deployment upgrades from KVStore-only storage to the new SQL-backed
metadata_store, this migration automatically copies all existing vector store
data (stores, files, file contents, and batches) into the SQL tables on first
server startup. The migration is idempotent — it only runs when the SQL tables
are empty and KVStore data exists.

Migrated records are stamped with owner_principal="" (unowned), making them
accessible to all authenticated users, which is correct for pre-multi-tenancy
data that had no ownership concept.

Uses only the KVStore/SqlStore protocol interfaces, so it works with any
backend (SQLite, Postgres, etc.) without backend-specific code.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>
…lve_impls

The vector_io KVStore->SQL migration runs during the provider adapter's
initialize() inside resolve_impls and issues a fetch_all that lazily
creates the shared SQLAlchemy engine. Because SqlAlchemySqlStoreImpl.create_table
only registers a Table in metadata and defers actual creation to the first
engine bind, any service whose initialize() runs after the engine is bound
will register tables that are never created. This caused register_connectors
to fail with "no such table: connectors" during startup of the auth
integration tests.

Move prompts, conversations, and connectors initialize() to before
resolve_impls so their tables are registered in metadata prior to any
provider initialize() triggering engine creation.

Also add the missing metadata_store field and policy parameter to the
inline qdrant provider for parity with the other inline vector_io providers.

Signed-off-by: Charlie Doern <cdoern@redhat.com>
- Tighten internal-init loop in stack.py to keep file under 1000-line limit.
- Apply ruff-format to inline qdrant __init__.
- Regenerate inline_qdrant.mdx provider doc for new metadata_store field.

Signed-off-by: Charlie Doern <cdoern@redhat.com>

@skamenan7 skamenan7 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, couple of comments. Thanks!

sql_store = self.metadata_store.sql_store

existing = await sql_store.fetch_all(table=TABLE_VECTOR_STORES, limit=1)
if existing.data:

@skamenan7 skamenan7 May 11, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the migration guard might be a bit too coarse here -- it checks vector_stores for any row, so if startup dies after copying stores but before files/chunks/batches, the next boot skips everything. a per-table check or a migration-complete flag would make this safe to resume after a crash.

resp = httpx.post(
f"{self.base_url}/v1/vector_stores",
headers=self._headers(),
json={"name": name},

@skamenan7 skamenan7 May 11, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be missing something, but it looks like line 60 only sends {"name": name} and the auth workflow doesn't set default_embedding_model? the oauth2_token job is showing 400 on create, which would make sense if the router needs embedding config before it gets to tenant isolation. would passing embedding fields in the request body work here?

franciscojavierarceo and others added 3 commits May 11, 2026 18:04
…store hook

Merge upstream/main which added prompts tenant isolation and an
AuthorizedSqlStore enforcement pre-commit hook. Both prompts and
vector store ABAC policies coexist in the auth CI workflow.

Migrated all 9 vector_io providers from direct sqlstore_impl usage
to the authorized_sqlstore() convenience function to satisfy the
new pre-commit hook.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>
Fix CI auth workflow failures and address review feedback:

- Add vector_store::* routing table ABAC rules alongside existing
  sql_record::vector_stores::* rules to cover both authorization layers
- Remove stale agent_state and metadata_store configs from the CI auth
  workflow to prevent SQLAlchemy lazy-init race with the responses table
- Add embedding_model parameter to vector store access control tests
- Use per-table migration guards in kvstore-to-sql migration so a crash
  mid-migration can resume from the last incomplete table on next boot

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>
@franciscojavierarceo
franciscojavierarceo force-pushed the multi-tenancy/pr4-vector-store-isolation branch from bf33fce to 820a843 Compare May 12, 2026 04:44
leseb added 2 commits May 12, 2026 11:20
…initialization.

Signed-off-by: Sébastien Han <seb@redhat.com>
Signed-off-by: Sébastien Han <seb@redhat.com>

@leseb leseb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pushed some commits in this branch since you're away :)

@leseb
leseb enabled auto-merge May 12, 2026 09:32
@leseb
leseb added this pull request to the merge queue May 12, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks May 12, 2026
Signed-off-by: Sébastien Han <seb@redhat.com>
@leseb
leseb enabled auto-merge May 12, 2026 09:55
@leseb
leseb added this pull request to the merge queue May 12, 2026
Merged via the queue into ogx-ai:main with commit ef8cf45 May 12, 2026
43 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.

4 participants