Loader.load() dispatches to the underlying langchain document loaders
(PyMuPDF, Unstructured, python-docx, Tika, …) which are all
synchronous and CPU/IO-bound. process_file() awaited it directly on
the event loop, so parsing a non-trivial PDF/DOCX would freeze the
entire FastAPI app for the duration of the parse — which is what users
experience as "the server hangs whenever I upload a file."
Add an `aload()` async wrapper on Loader that runs the sync load on a
worker thread via asyncio.to_thread, and update process_file() to
await it. The sync API is preserved so existing callers that already
run inside run_in_threadpool (e.g. save_docs_to_vector_db) are
unaffected.
https://claude.ai/code/session_01JSr4NZSskEUQvoJnavVXh8
Co-authored-by: Claude <noreply@anthropic.com>
* fix(retrieval): offload sync VECTOR_DB_CLIENT calls in async paths via AsyncVectorDBClient
The vector DB backends (Chroma, pgvector, Qdrant, Milvus, Pinecone,
Weaviate, …) are uniformly synchronous and their methods perform
blocking network or disk I/O. Multiple async route handlers and helpers
were calling them directly on the event loop — file processing,
memories, knowledge bases, hybrid search bookkeeping — so a single
upsert/delete/search would freeze every other in-flight request for the
duration of the call.
Introduce `AsyncVectorDBClient`, a thin async facade that wraps the
existing sync client and dispatches each method through
`asyncio.to_thread`. It mirrors `VectorDBBase` exactly and forwards
*args/**kwargs so backend-specific extra parameters keep working.
Update every async-context call site (routers/retrieval, routers/files,
routers/memories, routers/knowledge, retrieval/utils,
tools/builtin) to await `ASYNC_VECTOR_DB_CLIENT` instead of calling the
sync client directly. Two helpers that were sync-only also acquire
async siblings or are awaited via `asyncio.to_thread` at their async
call site (`remove_knowledge_base_metadata_embedding`,
`get_all_items_from_collections`, `query_doc`).
The original sync `VECTOR_DB_CLIENT` is unchanged, so callers that
already run inside `run_in_threadpool` (e.g. `save_docs_to_vector_db`
and the sync `query_doc`/`get_doc` helpers) are unaffected.
https://claude.ai/code/session_01JSr4NZSskEUQvoJnavVXh8
* fix(retrieval): restore explicit AsyncVectorDBClient signatures matching VectorDBBase
Per PR review: the original *args/**kwargs forwarding lost type
safety and IDE/static-analysis support. Restore explicit signatures
that mirror VectorDBBase exactly, so:
* Bad kwargs fail at the facade boundary instead of inside the
worker thread (where the resulting TypeError tends to be
swallowed by surrounding `try/except`).
* IDE autocomplete and static analysis work as expected.
* The stated intent ("mirror VectorDBBase exactly") now holds at
the API contract level, not just behaviourally.
While doing this, surface a pre-existing bug in
`delete_entries_from_collection` that the stricter typing flagged:
the call passed `metadata={'hash': hash}` which is not a parameter
on `VectorDBBase.delete` nor any backend. The TypeError raised
inside the sync delete was silently swallowed by `except Exception`
so the endpoint always reported `{'status': False}` for every
request instead of actually deleting matching vectors. Replace with
`filter=...` to do what the endpoint name promises.
The thorough review's other note (no concurrency/backpressure on
the shared default threadpool) is intentionally not addressed here:
asyncio.to_thread on the shared executor is the right primitive for
this use case; per-domain bounded executors would add lifecycle
complexity disproportionate to the problem and the loop is no
longer blocked, which was the actual bug.
https://claude.ai/code/session_01JSr4NZSskEUQvoJnavVXh8
* fix(retrieval): parallelize hybrid-search collection prefetch; document async facade contracts
Address PR review findings:
1. Hybrid-search prefetch was sequential
`query_collection_with_hybrid_search` previously awaited
`ASYNC_VECTOR_DB_CLIENT.get(name)` once per collection in a for
loop. Each call already off-loaded to a worker thread, but
awaiting them serially meant total prefetch latency scaled
linearly with the number of collections. Run them concurrently
with `asyncio.gather` so multi-collection queries actually
benefit from the threadpool. Per-collection exception handling
is preserved by wrapping each fetch in a small helper that
logs and returns `(name, None)` on failure, so a single bad
collection cannot poison the whole gather.
2. Document the thread-safety expectation explicitly
The facade now formally states what was always implicit: the
sync `VECTOR_DB_CLIENT` is shared across worker threads, so the
underlying backend driver must be thread-safe. This is not a
new exposure — `save_docs_to_vector_db` already called the sync
client from `run_in_threadpool`. Adding a global lock here
would defeat the responsiveness the facade exists to provide;
backends that cannot tolerate concurrent access should grow
their own internal serialization.
3. Document the API-surface choice and `.sync` escape hatch
The strict `VectorDBBase` mirror was a deliberate choice (the
previous `*args/**kwargs` revision let a `metadata=` typo
silently break an endpoint). Document it, and call out the
`.sync` escape hatch with an example for callers that genuinely
need a backend-specific parameter not on `VectorDBBase`.
https://claude.ai/code/session_01JSr4NZSskEUQvoJnavVXh8
* fix(retrieval): guard /delete against null file.hash and let HTTPException reach the client
Address PR review finding on the `metadata=` → `filter=` change in
`delete_entries_from_collection`.
The new `filter={'hash': hash}` query was correct for files that
have a hash, but did not handle `file.hash is None` (unprocessed,
failed, or legacy records). The match semantics of a null filter
value are backend-dependent — some ignore the key entirely, some
treat it as "metadata field absent" and match every such row — so
issuing the query risked deleting unrelated entries.
* Reject `hash is None` up front with a 400 explaining the file
has no hash to target.
* Narrow the surrounding `except Exception` so it no longer
swallows `HTTPException`. Without this fix the new 400 (and the
pre-existing 404 for missing files) would be silently re-shaped
into `{'status': False}` and the caller could not distinguish a
bad-request input from a backend error.
https://claude.ai/code/session_01JSr4NZSskEUQvoJnavVXh8
---------
Co-authored-by: Claude <noreply@anthropic.com>
Per RFC 4513, a Simple Bind with a non-empty DN but empty password is unauthenticated simple authentication. Many LDAP servers (OpenLDAP default, some AD configs) accept these binds, allowing account takeover without valid credentials.
Rejects empty and whitespace-only passwords before attempting the LDAP bind.
Unlike all other resource routers (knowledge, models, notes, prompts, tools, skills), the channel router did not call filter_allowed_access_grants. This allowed any user to set wildcard access grants on group channels, bypassing the admin's public sharing permission framework.
Adds filter_allowed_access_grants with the sharing.public_channels permission key to both create and update endpoints, matching the pattern used by all other resource routers.
The OAuth token exchange endpoint skipped the domain allowlist check that the normal OAuth callback enforces. An attacker with a valid OAuth token from a non-allowed domain (e.g. gmail.com) could bypass the admin's domain restriction policy entirely.
Adds the same domain validation check used in the OAuth callback, denying access when the email domain is not in the allowed list.
SESSION_POOL caches user.role at connection time and never refreshes it. When an admin demotes or deletes a user, their socket sessions retain the old cached role until voluntary disconnect, allowing continued use of admin-gated socket features (ydoc editing, channel access).
Adds disconnect_user_sessions() helper that disconnects all sockets for a user ID. Called from update_user_by_id (on role change) and delete_user_by_id. The client auto-reconnects and re-authenticates with fresh DB data.
The catch-all /{path:path} proxy forwards any request to the upstream OpenAI-compatible API with the admin's API key and no access control. This is an intentional proxy but should be opt-in.
Adds ENABLE_OPENAI_API_PASSTHROUGH env var (defaults to False). When disabled, the catch-all returns 403. No other routers (Ollama, responses) have catch-all proxies.
The GET /channels/{id}/members endpoint checked membership for group/dm channels but had no access gate for standard channels, allowing any authenticated user with channels permission to enumerate members of private standard channels by UUID.
The model name from user input was interpolated directly into Azure deployment URL paths without validation. A user could send a model name like '../../management/foo' to traverse the URL path and hit unintended Azure endpoints with the admin's API key.
Adds _sanitize_model_for_url that rejects path separators and traversal sequences, and percent-encodes the name. Applied at convert_to_azure_payload (covers chat completions + proxy) and the responses endpoint's direct URL construction.
These four endpoints checked model existence but never verified the user has read access via AccessGrants, allowing any authenticated user to use restricted models.
Uses the canonical check_model_access helper from utils.access_control.
Both LDAP and OAuth registration checked user count before insert to determine whether to assign admin role. With multiple workers, concurrent first-user registrations could each see zero users and both create admin accounts.
Applies the insert-first-check-after pattern already used by signup_handler: insert with DEFAULT_USER_ROLE, then atomically check get_num_users()==1 and promote only the sole user to admin.
The /responses proxy endpoint only required authentication via
get_verified_user but did not check per-model access grants. This
allowed any authenticated user to access any model through this
endpoint, bypassing the access control system.
Extract a shared check_model_access helper into utils/access_control
and replace all inline access control blocks across openai.py and
ollama.py (7 locations) with calls to this helper. This eliminates
code duplication and prevents future policy drift between endpoints.
CWE-862: Missing Authorization
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:H (6.5 Medium)
Using user.id as client_id causes WebSocket deadlocks when the same
user generates images concurrently (e.g., multi-model chat). ComfyUI
routes messages by clientId, so shared IDs mean only one connection
receives the completion — others hang forever.
Generate a unique UUID per request, matching ComfyUI's own examples.
Ollama recently added Responses API support via its OpenAI-compatible
endpoint (/v1/responses). This adds a proxy endpoint to the Ollama
router that forwards requests to Ollama's /v1/responses, applying
the same model resolution, access control, and prefix_id handling
used by the existing /v1/chat/completions and /v1/messages proxies.
Uses a typed ResponsesForm Pydantic model with required model field
and extra='allow' for forward compatibility, consistent with other
endpoint schemas in the file.
This allows API consumers (Codex, Claude Code, etc.) to use the
Responses API directly with Ollama-hosted models without requiring
a separate OpenAI-compatible connection.
Azure offers two URL formats: the legacy deployment-based format
(/openai/deployments/{model}/...) and the newer v1 format
(/openai/v1/...) where the model stays in the payload body and no
api-version query parameter is needed.
Previously, the code always ran convert_to_azure_payload which
rewrites the URL to the deployment format, causing 404 errors for
users with v1-style base URLs. Now, when the base URL contains
'/openai/v1', we skip deployment URL construction and route
directly.
Applied consistently across all three Azure routing paths:
generate_chat_completion, /responses proxy, and generic proxy.