Files
LangBot/src/langbot/pkg/vector/vdb.py
T
RockChinQanddadachann e1ac5e0fc8 feat(tenancy): add Workspace multi-tenant foundation (#2353)
* Document multi-tenant workspace architecture

* Add OSS and commercial workspace boundaries

* docs: redesign multi-tenant workspace architecture

* feat(tenancy): implement workspace isolation

* docs(tenancy): record verification evidence

* docs(tenancy): revise single-instance SaaS topology

* docs(tenancy): refine architecture options

* docs: finalize cloud v2 multi-tenant decisions

* feat(tenancy): establish cloud isolation foundations

* feat(tenancy): harden shared cloud runtime boundaries

* docs(tenancy): record final isolation verification

* fix(tenancy): close isolation and permission gaps

* docs(tenancy): record final isolation verification

* feat(tenancy): connect cloud workspace control plane

* fix(build): install git for pinned SDK

* docs(cloud): update control plane verification

* chore: update multi-tenant SDK pin

* fix(cloud): skip legacy model sync during startup

* test(cloud): preserve minimal model manager fixtures

* fix(cloud): preserve authenticated account context

* fix(cloud): reuse authenticated account for user info

* feat(cloud): complete Workspace settings navigation

* test(web): cover Workspace dropdown menu

* feat(web): place workspace controls in sidebar

* refactor(web): streamline workspace controls

* style(web): format workspace layout test

* fix(cloud): surface runtime and workspace plan status

* fix(plugin): keep runtime identity stable across restarts

* fix(ui): widen and center workspace switcher

* fix(ui): hide roles from workspace switcher

* fix(ui): align workspace switcher with sidebar entries

* feat(workspace): add in-product collaboration and direct Cloud launch

* style: format collaboration changes

* fix(workspace): bind collaboration APIs to tenant UoW

* fix(cloud): preserve Core-owned collaboration state

* test(cloud): require Space identity for invite registration

* feat(cloud): complete secure invitation experience

* style(web): format invitation flows

* fix(cloud): recover box runtime without unscoped skill reload

* feat(oss): enforce invitation account and owner billing flows

* style: format OSS account service

* test(oss): cover invitation logout handoff

* fix(oss): resolve workspace owner in scoped session

* feat(cloud): harden multi-tenant runtime resources

* fix(cloud): bound runtime restart storms

* fix(cloud): eliminate periodic runtime CPU spikes

* fix(cloud): enforce instance capacity ceilings

* fix(cloud): scope public login capability discovery

* fix(cloud): bound tenant maintenance and monitoring work

* fix(runtime): bound tenant resource amplification

* fix(deps): pin green multi-tenant plugin SDK

* fix(cloud): handle unavailable skill capability

* fix(security): require authentication for image file endpoint (H-2)

- Changed /api/v1/files/image from AuthType.NONE to USER_TOKEN_OR_API_KEY
- Added Permission.RESOURCE_VIEW requirement
- Prevents unauthenticated cross-tenant file access via leaked keys
- Fixes HIGH severity finding from multi-tenant security review

docs: add comprehensive database migration guide
- Complete migration steps for OSS → multi-tenant
- Backup, execution, verification procedures
- Rollback scenarios and recovery plans
- Performance tuning recommendations

* test: add comprehensive cross-tenant isolation tests

Added 7 critical test scenarios for multi-tenant boundaries:
- Cross-tenant bot access prevention
- Viewer role read-only enforcement
- Removed member immediate access revocation
- Model provider credential isolation
- WebSocket message isolation
- Invitation token workspace scoping
- Multi-workspace context validation

These tests address P0-2 coverage gaps for:
- workspaces.py (membership & invitation flows)
- user.py (authentication & authorization)
- websocket_chat.py (real-time isolation)
- plugins.py (resource access control)

docs: finalize database migration guide

* fix(security): resolve M-1, M-2, M-3 security findings

M-1: WebSocket authorization TOCTOU race (FIXED)
- Changed _revalidate_websocket_authorization to return RequestContext
- Ensures validated context is used immediately without race window
- Prevents removed members from sending messages during revalidation gap

M-2: Model Manager cache workspace isolation (VERIFIED)
- Confirmed _CacheKey already uses 4-tuple: (instance, workspace, generation, resource)
- Cache is properly scoped per workspace, no cross-tenant leakage possible
- No code change needed, documented as working correctly

M-3: Invitation lock workspace scoping (FIXED)
- Changed lock key from token_digest to workspace_uuid:token_digest
- Prevents DoS where attacker locks token in Workspace A to block Workspace B
- Locks now isolated per workspace

All MEDIUM severity findings from security review now resolved.

* fix(cloud): unblock tenant CI and enforce knowledge quotas

* fix(tenancy): scope rerank model sync

---------

Co-authored-by: dadachann <[email protected]>
2026-07-30 21:43:35 +08:00

160 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import abc
import enum
from typing import Any, Dict, MutableMapping, MutableSet, TypeVar
import numpy as np
_CacheValue = TypeVar('_CacheValue')
def runtime_cache_limit(ap: Any, default: int = 1024) -> int:
try:
value = int(ap.instance_config.data.get('vdb', {}).get('runtime_cache_limit', default))
except (AttributeError, TypeError, ValueError):
value = default
return max(value, 1)
def remember_bounded_mapping(
cache: MutableMapping[str, _CacheValue],
key: str,
value: _CacheValue,
limit: int,
) -> None:
cache.pop(key, None)
cache[key] = value
while len(cache) > limit:
cache.pop(next(iter(cache)))
def remember_bounded_set(cache: MutableSet[str], key: str, limit: int) -> None:
cache.add(key)
while len(cache) > limit:
cache.pop()
class SearchType(str, enum.Enum):
"""Supported search types for vector databases."""
VECTOR = 'vector'
FULL_TEXT = 'full_text'
HYBRID = 'hybrid'
class VectorDatabase(abc.ABC):
async def close(self) -> None:
"""Release backend clients and in-process caches."""
@classmethod
def supported_search_types(cls) -> list[SearchType]:
"""Return the search types supported by this VDB backend.
Default: vector search only. Override in subclasses that support
full-text or hybrid search.
"""
return [SearchType.VECTOR]
@abc.abstractmethod
async def add_embeddings(
self,
collection: str,
ids: list[str],
embeddings_list: list[list[float]],
metadatas: list[dict[str, Any]],
documents: list[str] | None = None,
) -> None:
"""Add vector data to the specified collection.
Args:
collection: Collection name.
ids: Unique IDs for each vector.
embeddings_list: List of embedding vectors.
metadatas: List of metadata dicts.
documents: Optional raw text documents. Required for full-text
and hybrid search in backends that support them.
"""
pass
@abc.abstractmethod
async def search(
self,
collection: str,
query_embedding: np.ndarray,
k: int = 5,
search_type: str = 'vector',
query_text: str = '',
filter: dict[str, Any] | None = None,
vector_weight: float | None = None,
) -> Dict[str, Any]:
"""Search for the most similar vectors in the specified collection.
Args:
collection: Collection name.
query_embedding: Query vector for similarity search.
k: Number of results to return.
search_type: One of 'vector', 'full_text', 'hybrid'.
query_text: Raw query text, used for full_text and hybrid search.
filter: Optional metadata filters using Chroma-style ``where``
syntax. Multiple top-level keys are AND-ed. Supported
operators: ``$eq``, ``$ne``, ``$gt``, ``$gte``, ``$lt``,
``$lte``, ``$in``, ``$nin``. Example::
{"file_id": "abc"}
{"created_at": {"$gte": 1700000000}}
{"file_type": {"$in": ["pdf", "docx"]}}
vector_weight: Weight for vector search in hybrid mode (0.0–1.0).
``None`` means use equal weights (backward compatible).
"""
pass
@abc.abstractmethod
async def delete_by_file_id(self, collection: str, file_id: str) -> None:
"""Delete vectors from the specified collection by file_id."""
pass
@abc.abstractmethod
async def delete_by_filter(self, collection: str, filter: dict[str, Any]) -> int:
"""Delete vectors matching the given metadata filter.
Args:
collection: Collection name.
filter: Metadata filter dict in canonical format (see ``search``).
Returns:
Number of deleted vectors (best-effort; backends that cannot
report an exact count may return 0).
"""
pass
async def list_by_filter(
self,
collection: str,
filter: dict[str, Any] | None = None,
limit: int = 20,
offset: int = 0,
) -> tuple[list[dict[str, Any]], int]:
"""List vectors matching the given metadata filter with pagination.
Args:
collection: Collection name.
filter: Optional metadata filter dict in canonical format.
limit: Maximum number of items to return.
offset: Number of items to skip.
Returns:
Tuple of (items, total) where items is a list of dicts with
keys 'id', 'document', 'metadata', and total is the best-effort
count of all matching vectors (-1 if unknown).
"""
return [], -1
@abc.abstractmethod
async def get_or_create_collection(self, collection: str):
"""Get or create collection."""
pass
@abc.abstractmethod
async def delete_collection(self, collection: str):
"""Delete collection."""
pass