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 only — QueryService.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
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"
Context
The
learn/agentos/cloud-deploymentstaleness audit (Origin Session below) foundSecurity.md:9,33asserts that a tenant'sprivatecontent is filtered out of other queries — i.e. it documents read-sidevisibilityenforcement. 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 — aprivatechunk in that shared tenant currently leaks to every reader of it.The Problem
KB reads filter on
tenantId+typeonly —QueryService.mjs:125-127andDocumentService.mjs:57,97buildwhere: {tenantId: {$in: [normalizeUserId(getUserId()), defaultTenantId]}}(+ optionaltype). Thevisibilityfield 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). Sovisibility:'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 —
privateleaks. Memory Core already enforces visibility on its reads (GraphService.mjs/ SQLite$oroveruserId/SHARED_USER_ID+ thememorySharingpolicy); KB should mirror that.The Architectural Reality
ai/services/knowledge-base/QueryService.mjs:125-127,DocumentService.mjs:57,97.ai/services/knowledge-base/VectorService.mjs:136-150(resolveTenantStampstampstenantId,repoSlug,visibility, optionaloriginAgentIdentity).privatechunk = its stampedoriginAgentIdentity(the writer).$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'sprivate— e.g.{tenantId: {$in: [...]}, $or: [{visibility: {$ne: 'private'}}, {originAgentIdentity: <requester>}]}(mirroring MC's$or). Apply in bothQueryService.queryDocumentsand theDocumentServiceread paths. Preserve the offline/no-auth null-filter branch (no request context → no filter, byte-equivalent with pre-#11632). Define a fail-safe for aprivatechunk lackingoriginAgentIdentity(treat as non-readable by non-owners). UpdateSecurity.mdto describe the now-true enforcement.Contract Ledger
QueryService.queryDocuments,DocumentServicegetters)ai/services/knowledge-base/QueryService.mjs:125-127visibility$orgate:privatechunks returned only to theiroriginAgentIdentitycloud-deployment/Security.md(made accurate)QueryService.mjs:127,VectorService.mjs:139, Memory Core read-side visibility$orDecision Record impact
aligned-withthe documented tenant-isolation security model (Security.md). No ADR conflict — makes the read path enforce what the security doc + the stampedvisibilityfield already imply.Acceptance Criteria
visibility:'private'chunk is NOT returned to a same-tenant requester who is not itsoriginAgentIdentity(the leak is closed).visibility:'team'chunk is still returned to any same-tenant requester.privatechunk IS returned to its ownoriginAgentIdentity.privatechunk lacking anoriginAgentIdentityfails safe (not returned to non-owners).KnowledgeBase.TenantIsolation.spec.mjsnull-context assertion stays green).QueryServiceandDocumentServiceread paths.Security.md:9,33updated to describe the actual (now-enforced) behavior.MemoryService.TenantIsolation.spec.mjs's visibility assertions.Out of Scope
unified-store rename (#12153).Related
Security.mdclaim real).learn/agentos/cloud-deployment/Security.md(the overclaim this resolves); Memory CoreGraphServicevisibility$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"