feat(vector-sync): honor Astrolabe admin consent for searchable sources

Consume the astrolabe.semantic_search capability as the source of truth for
which content sources an admin has approved for semantic search, and enforce
it independently of Astrolabe (this server queries Qdrant directly).

- capabilities.py: cached per-user reader for enabled_doc_types (TTL+LRU,
  fail-open so older Astrolabe / transient OCS errors don't break search)
- semantic search: intersect requested doc_types with the allowed set;
  restrict to the allowed set when none requested; short-circuit when empty
- scanner: skip disabled sources during discovery (files discovery yields
  nothing when disabled, so the existing grace-period reconcile purges them)
- processor: drop near-real-time index tasks for disabled doc_types
  (webhook events bypass the scanner gate); deletes always proceed
- vector/purge.py + POST /api/v1/vector-sync/purge: admin-only global
  delete-by-doc_type, called by Astrolabe when a source is disabled so
  consent is binding on data-at-rest

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-16 00:38:35 +02:00
co-authored by Claude Opus 4.8
parent 07ee91399b
commit ef5b3f3873
11 changed files with 770 additions and 6 deletions
+28
View File
@@ -20,6 +20,7 @@ from mcp.types import (
from pydantic import Field
from nextcloud_mcp_server.auth import require_scopes
from nextcloud_mcp_server.capabilities import allowed_doc_types
from nextcloud_mcp_server.config import get_settings
from nextcloud_mcp_server.context import get_client
from nextcloud_mcp_server.models.semantic import (
@@ -300,6 +301,33 @@ def configure_semantic_tools(mcp: FastMCP):
# files under their own user_id.
accessible_owners = await list_accessible_owners(client.sharing, username)
# Admin consent gate: restrict to source types the Astrolabe admin has
# approved (and that are installed for this user). This mirrors
# Astrolabe's own server-side enforcement but is independent because
# this tool queries Qdrant directly. ``None`` = no restriction
# (fail-open / Astrolabe predating this feature). An empty allow-set
# means the admin disabled every source.
allowed = await allowed_doc_types(client, username)
if allowed is not None:
if doc_types is None:
doc_types = sorted(allowed)
else:
doc_types = [dt for dt in doc_types if dt in allowed]
if not doc_types:
logger.info(
"Semantic search short-circuited for user %s: no requested "
"doc_type is both installed and admin-approved",
username,
)
return SemanticSearchResponse(
results=[],
query=query,
total_found=0,
search_method=f"bm25_hybrid_{fusion}",
verified_chunk_count=0,
dropped_document_count=0,
)
try:
# The nc_semantic_search tool deliberately uses BM25-hybrid (dense +
# sparse with RRF/DBSF fusion) as the single tool-layer algorithm.