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:
co-authored by
Claude Opus 4.8
parent
07ee91399b
commit
ef5b3f3873
@@ -21,6 +21,7 @@ if TYPE_CHECKING:
|
||||
from nextcloud_mcp_server.document_processors.registry import ProcessorRegistry
|
||||
|
||||
from nextcloud_mcp_server.acl_hash import compute_acl_hash
|
||||
from nextcloud_mcp_server.capabilities import allowed_doc_types, is_doc_type_allowed
|
||||
from nextcloud_mcp_server.client import NextcloudClient
|
||||
from nextcloud_mcp_server.config import get_settings
|
||||
from nextcloud_mcp_server.embedding import get_bm25_service, get_embedding_service
|
||||
@@ -574,6 +575,24 @@ async def process_document(
|
||||
):
|
||||
await _reconcile_tag_event(doc_task, nc_client)
|
||||
|
||||
# Admin consent gate (Astrolabe): never index a source the admin has
|
||||
# disabled for semantic search — this catches near-real-time webhook
|
||||
# events that bypass the scanner's discovery gate. Deletes always
|
||||
# proceed (removing data honours consent). ``None`` from the reader
|
||||
# means no restriction (fail-open / older Astrolabe), so a transient
|
||||
# capabilities failure never silently drops indexing.
|
||||
if doc_task.operation == "index":
|
||||
allowed = await allowed_doc_types(nc_client, doc_task.user_id)
|
||||
if not is_doc_type_allowed(doc_task.doc_type, allowed):
|
||||
logger.info(
|
||||
"Skipping index of %s_%s for %s: doc_type disabled by admin",
|
||||
doc_task.doc_type,
|
||||
doc_task.doc_id,
|
||||
doc_task.user_id,
|
||||
)
|
||||
record_vector_sync_processing(time.time() - start_time, "skipped")
|
||||
return
|
||||
|
||||
# Handle deletion
|
||||
if doc_task.operation == "delete":
|
||||
# Release this user rather than blind-delete: a file shared across
|
||||
|
||||
Reference in New Issue
Block a user