Feature/workspace isolation#3011
Open
disillusioners wants to merge 32 commits intoHKUDS:mainfrom
Open
Conversation
…isolation Phase 1: - Add WorkspaceManager class with LRU cache (max 10 instances), reference counting, per-workspace async locking, and safe eviction - Add WorkspaceCapacityError for capacity overflow - Add sanitize_workspace_name() utility in api/utils.py - Add comprehensive unit tests (26 tests) Phase 2: - Create factory callable in lightrag_server.py capturing all 25 LightRAG constructor args - Replace single rag instance with WorkspaceManager - Add FastAPI lifespan handler for startup pre-warm and shutdown cleanup - Update route factory signatures to accept workspace_mgr - Update /health endpoint to use WorkspaceManager with try/finally release - Reduce Neo4j default connection pool from 100 to 10 - Audit _default_workspace usage (verified safe, documented)
…e reporting - Wire sanitize_workspace_name() into get_workspace_from_request() (C2) - Add defensive sanitization call in WorkspaceManager.get_or_create() - Move _finalize_instance() outside global lock in _evict_one() and shutdown() (C3) - Document pre-warm ref_count=1 design choice (W3) - Fix /health endpoint to report actual queried workspace (W4)
…n via WorkspaceManager
Add comprehensive integration tests for workspace isolation at the HTTP API layer using httpx.AsyncClient with ASGITransport. Tests verify: - Header-based workspace extraction from LIGHTRAG-WORKSPACE header - Default workspace fallback (empty string) when no header present - Workspace name validation (special chars, path traversal, length) - Concurrent request isolation across different workspaces - Background task pattern with proper ref count management - Streaming response pattern with ref held during stream - Capacity limit enforcement returning HTTP 503 - LRU eviction under concurrent load Also update conftest.py to allow @pytest.mark.offline tests in tests/integration/ to run without --run-integration flag.
…eError - memgraph_impl.py: initialize memgraph_workspace and original_workspace to None before conditional block - neo4j_impl.py: initialize original_workspace to None before conditional block (neo4j_workspace was already fixed) These variables were only assigned inside conditional blocks but referenced unconditionally in logging statements, causing NameError when WORKSPACE_ISOLATION=true.
- Add WorkspaceSelector dropdown component with auto-refresh - Add currentWorkspace state to settings store with v20 migration - Add Workspace type and getWorkspaces() API function - Inject LIGHTRAG-WORKSPACE header conditionally in axios interceptor and streaming fetch - Integrate selector into SiteHeader with proper separator handling - Add workspace i18n keys to English locale
- C1: Sanitize LIGHTRAG-WORKSPACE header to prevent CRLF injection - C2: Add malformed response guard in getWorkspaces() - W3: Reset stale workspace selection when workspace removed server-side
- Add workspace API tests (sanitizeHeader, getWorkspaces, header injection) - Add WorkspaceSelector component logic tests (fetch, stale detection, change handling) - Add settings store migration tests - Export sanitizeHeader and axiosInstance for testability - Add testing dependencies: @testing-library/react, @testing-library/jest-dom, @playwright/test, happy-dom, playwright
- Remove folder icon from workspace selector dropdown - Add tooltip on hover showing "Workspace" label - Add spacing between LightRAG title and workspace selector
- Add useWorkspaceChange hook that monitors workspace changes and clears state - Documents: clear and re-fetch document list on workspace change - Knowledge Graph: reset graph state including isFetching flag - Retrieval: clear query messages and history on workspace change - Add workspaceRefreshTrigger signal in settings store - API tab confirmed workspace-agnostic (no changes needed) Files modified: - src/stores/settings.ts (workspaceRefreshTrigger + triggerWorkspaceRefresh) - src/stores/graph.ts (isFetching: false in reset) - src/hooks/useWorkspaceChange.ts (new) - src/App.tsx (useWorkspaceChange hook) - src/features/DocumentManager.tsx (workspace refresh handling) - src/features/RetrievalTesting.tsx (clear messages on workspace change)
- Add partialize to settings persist config to exclude trigger counters from localStorage, preventing stale refresh on page reload - Move graphDataFetchAttempted/labelsFetchAttempted resets and incrementGraphDataVersion into graph.reset() for completeness - Remove now-redundant manual calls from useWorkspaceChange hook
… functionality Add comprehensive tests for workspace isolation features including: - workspaceRefreshTrigger state and triggerWorkspaceRefresh() in settings store - searchLabelDropdownRefreshTrigger state and triggerSearchLabelDropdownRefresh() in settings store - useWorkspaceChange hook behavior - graph store workspace isolation
…ration Include /workspaces in the VITE_API_ENDPOINTS environment variable to ensure the development server correctly proxies workspace-related API requests.
The root cause was state.reset() being called inside the fetch completion handler (useLightragGraph.tsx line 377). reset() sets graphDataFetchAttempted to false, which re-triggers the fetch useEffect that checks that flag. The fix replaces state.reset() with targeted clears that preserve the fetch attempt flags (graphDataFetchAttempted, labelsFetchAttempted), preventing the fetch useEffect from re-triggering after a successful fetch. Fetch flags are only reset by the workspace change handler (useWorkspaceChange), which is the correct place for full state reset.
The previous fix for the infinite loop (commit 3cc3613) prevented state.reset() from being called in the fetch completion handler. But this broke workspace switching: after calling reset(), the fetch useEffect never re-fired because none of its React dependencies actually changed. Root cause: Two issues after workspace change: 1. graphDataVersion was not incremented, so the fetch useEffect's dependency array didn't change (isFetching was already false) 2. queryLabel stayed empty ('') because the previous fetch handler cleared it when graph data was empty. The emptyDataHandledRef guard then blocked re-fetching. Fix: In useWorkspaceChange, after calling reset(): - Call incrementGraphDataVersion() to trigger the fetch useEffect - Call setQueryLabel(defaultQueryLabel) to restore '*' so the fetch path is entered (avoids emptyDataHandledRef guard) Verified with Playwright E2E: - Initial load: 1 /graphs call - Switch workspace: 1 /graphs call (was 0 before fix) - Switch back: 1 /graphs call (was 0 before fix) - No infinite loop: 0 calls during 15s watch periods - All 86 unit tests pass
The workspace change useEffect was calling fetchPopularLabels() without await, causing bumpDropdownData() to trigger AsyncSelect remount BEFORE the popular labels were fetched and stored in SearchHistoryManager. This resulted in the combobox reading stale/empty data. Fixed by awaiting the fetchPopularLabels() call before triggering the dropdown refresh, ensuring SearchHistoryManager is populated before the component remounts and re-reads the data.
…CACHE_LIMIT env var The LRU cache limit for workspace RAG instances was hardcoded to 10. Now configurable via LIGHTRAG_WORKSPACE_CACHE_LIMIT environment variable. Defaults to 10. Invalid/non-numeric/negative values fall back to 10.
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.
Description
Adds workspace-based data isolation across all 13 LightRAG storage backends, enabling safe multi-tenant deployments where each
LightRAGinstance operates in its own isolated data space.Every LightRAG instance can now be assigned an immutable
workspaceidentifier. All data — entities, relations, documents, indexes — is namespaced under that workspace, preventing cross-tenant data access or collision.Supports all 13 storage backends:
Isolation strategy varies by storage type:
{workspace}:{namespace}prefix on entity/relation identifiers. Each workspace's data is isolated at the key/ID level within the same physical store.self.workspacepath. Each workspace gets its own data directory.Workspace lifecycle:
LightRAG(working_dir=..., workspace="tenant-a")construction timea-z,0-9,-,_), length limitsAdministrators of server-based backends can leverage existing environment variable controls (e.g.,
WORKSPACE_ISOLATION,{BACKEND}_WORKSPACE) alongside this feature.Related Issues
Changes Made
workspaceparameter toLightRAGconstructor for multi-tenant data isolation{workspace}:{namespace}) for shared storage backends (Neo4j, Memgraph, PostgreSQL, MongoDB, Redis, OpenSearch)WORKSPACE_ISOLATION,{BACKEND}_WORKSPACE)workspacecontinues to work identicallyChecklist
Additional Notes
Test coverage: 3 test files, 1,653 lines total:
test_workspace_isolation.pytest_workspace_migration_isolation.pytest_workspace_sanitization.pyBackward compatibility: Fully backward compatible. Internal separator differences between backends (
:vs_) and empty-workspace normalization are preserved for compatibility.Usage example: