LearnNewsExamplesServices
Frontmatter
id12163
titleEnforce chunk visibility on Knowledge Base reads
stateClosed
labels
bugaiarchitecture
assigneesneo-opus-ada
createdAtMay 29, 2026, 4:22 AM
updatedAtJun 1, 2026, 10:36 AM
githubUrlhttps://github.com/neomjs/neo/issues/12163
authorneo-opus-ada
commentsCount2
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 1, 2026, 10:36 AM

Enforce chunk visibility on Knowledge Base reads

Closed v13.0.0/archive-v13-0-0-chunk-14 bugaiarchitecture
neo-opus-ada
neo-opus-ada commented on May 29, 2026, 4:22 AM

Context

The learn/agentos/cloud-deployment staleness audit (Origin Session below) found Security.md:9,33 asserts that a tenant's private content is filtered out of other queries — i.e. it documents read-side visibility enforcement. The KB read path does not implement it. Rather than weaken the doc to match the code, make the code match the doc — the documented posture is the correct one.

This is now live-relevant: the first cloud deployment uses a shared default tenant (NEO_KB_DEFAULT_TENANT_ID) so multiple users resolve to the same tenant — a private chunk in that shared tenant currently leaks to every reader of it.

The Problem

KB reads filter on tenantId + type onlyQueryService.mjs:125-127 and DocumentService.mjs:57,97 build where: {tenantId: {$in: [normalizeUserId(getUserId()), defaultTenantId]}} (+ optional type). The visibility field is stamped on every chunk at write time (VectorService.resolveTenantStamp, default 'team', can be 'private') but is never consulted on any KB read path (verified — no QueryService / DocumentService / SearchService reference). So visibility:'private' is inert on reads: any same-tenant requester sees it.

Benign while each user is their own tenant (tenant == OIDC username → tenant == owner), but the moment a tenant is shared across users — the deployment's default-tenant flat-shared tier, or the future membership model — private leaks. Memory Core already enforces visibility on its reads (GraphService.mjs / SQLite $or over userId / SHARED_USER_ID + the memorySharing policy); KB should mirror that.

The Architectural Reality

  • Read filters: ai/services/knowledge-base/QueryService.mjs:125-127, DocumentService.mjs:57,97.
  • Write stamp: ai/services/knowledge-base/VectorService.mjs:136-150 (resolveTenantStamp stamps tenantId, repoSlug, visibility, optional originAgentIdentity).
  • Owner identity for a private chunk = its stamped originAgentIdentity (the writer).
  • MC precedent to mirror: Memory Core's read-side visibility $or.

The Fix

Extend the KB read where-clause to gate visibility: a chunk is returned only if its tenant matches and it is not someone else's private — e.g. {tenantId: {$in: [...]}, $or: [{visibility: {$ne: 'private'}}, {originAgentIdentity: <requester>}]} (mirroring MC's $or). Apply in both QueryService.queryDocuments and the DocumentService read paths. Preserve the offline/no-auth null-filter branch (no request context → no filter, byte-equivalent with pre-#11632). Define a fail-safe for a private chunk lacking originAgentIdentity (treat as non-readable by non-owners). Update Security.md to describe the now-true enforcement.

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
KB read filter (QueryService.queryDocuments, DocumentService getters) ai/services/knowledge-base/QueryService.mjs:125-127 adds a visibility $or gate: private chunks returned only to their originAgentIdentity offline/no-auth → no filter (unchanged) cloud-deployment/Security.md (made accurate) QueryService.mjs:127, VectorService.mjs:139, Memory Core read-side visibility $or

Decision Record impact

aligned-with the documented tenant-isolation security model (Security.md). No ADR conflict — makes the read path enforce what the security doc + the stamped visibility field already imply.

Acceptance Criteria

  • A visibility:'private' chunk is NOT returned to a same-tenant requester who is not its originAgentIdentity (the leak is closed).
  • A visibility:'team' chunk is still returned to any same-tenant requester.
  • A private chunk IS returned to its own originAgentIdentity.
  • A private chunk lacking an originAgentIdentity fails safe (not returned to non-owners).
  • Offline/no-auth path (no request context) still applies no filter — byte-equivalent with pre-#11632 (existing KnowledgeBase.TenantIsolation.spec.mjs null-context assertion stays green).
  • Applied in both QueryService and DocumentService read paths.
  • Security.md:9,33 updated to describe the actual (now-enforced) behavior.
  • Test coverage mirroring MemoryService.TenantIsolation.spec.mjs's visibility assertions.

Out of Scope

  • The user→tenant membership model (which tenants a requester sees) — the orthogonal RLS axis under design in Discussion #12161. This ticket is the visibility axis (owner-vs-team within a tenant); the two compose.
  • The Chroma unified-store rename (#12153).

Related

  • Discussion #12161 (KB tenant-membership) — orthogonal RLS axis; both improve KB read isolation. #12161 folded visibility-enforcement in as a companion; this ticket splits it out per operator direction (make the Security.md claim real).
  • learn/agentos/cloud-deployment/Security.md (the overclaim this resolves); Memory Core GraphService visibility $or (the precedent to mirror).

Origin Session ID: efd8dc2e-2052-4089-814a-ab22cd8c6a62 Retrieval Hint: "KB read-side visibility enforcement private chunk leak shared tenant mirror memory-core $or"