LearnNewsExamplesServices
Frontmatter
id11632
titlePhase 0/1D — KB Tenant Isolation Read-Side: QueryService/SearchService where-Filter + Fail-Closed Test Suite
stateClosed
labels
enhancementaitestingarchitecture
assigneesneo-opus-ada
createdAtMay 19, 2026, 1:54 PM
updatedAtJun 7, 2026, 7:13 PM
githubUrlhttps://github.com/neomjs/neo/issues/11632
authorneo-opus-ada
commentsCount0
parentIssue11625
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[x] 11631 Phase 0/1C — KB Tenant Isolation Write-Side: VectorService Server-Stamping + Tenant-Aware chunkId + Spoof-Rejection
blocking[x] 11645 Phase 5B — KB Tenant Isolation Integration Tests (post-Phase-0/1D)
closedAtMay 20, 2026, 11:03 AM

Phase 0/1D — KB Tenant Isolation Read-Side: QueryService/SearchService where-Filter + Fail-Closed Test Suite

Closed v13.0.0/archive-v13-0-0-chunk-12 enhancementaitestingarchitecture
neo-opus-ada
neo-opus-ada commented on May 19, 2026, 1:54 PM

Context

Sub of Phase 0/1 Epic #11625 (meta-Epic #11624). Graduated from Discussion #11623 Cycle 2.5/2.6.

Closes the Phase 0/1 security boundary — read-side tenant filter + end-to-end fail-closed test suite validates write-side stamping (#11631 Phase 0/1C) actually works.

Topology anchor: Per ADR 0003 — Chroma Topology Unified Only, this work injects tenant filters into collection.query calls against the knowledge-base collection in the unified Chroma daemon. Memory Core's MemoryService.queryMemories + SummaryService.querySummaries filter neo-agent-memory / neo-agent-sessions separately — same pattern, separate collections.

The Problem

V-B-A confirmed (Cycle 2 GPT): QueryService.mjs:116-128 builds Chroma where clause ONLY from type predicate; no tenant/visibility filter. Without read-side enforcement, even with write-side server-stamping (Phase 0/1C), every tenant retrieves every other tenant's content.

The Fix

Read-side filter

In ai/services/knowledge-base/QueryService.mjs and SearchService.mjs:

  1. Every collection.query() call injects where: {tenantId: {$in: [<requester>, '<team-namespace>']}} derived from authenticated AgentIdentity context
  2. Filter context server-side, NOT client payload
  3. Team-namespace constant (e.g., 'neo-shared') hardcoded for Neo curated content visibility across tenants

Mirrors MemoryService.mjs:391-410 query-time policy filter pattern.

Q13b enforcement-layer V1 lean

Per Discussion #11623 §4 Q13b — V1 implements application-layer filter (Option A); V2 reconsiders Chroma-layer hardening if cross-tenant leak class manifests in production. Phase 0/1D ships Option A.

Fail-Closed Test Suite

The load-bearing security test substrate (8 cases per Discussion §8 +7 absorption):

  1. Tenant isolation: tenant A cannot retrieve tenant B private chunks via query_documents / ask_knowledge_base / any public KB facade
  2. Team visibility: Neo 'neo-shared' chunks visible across tenants (regression-test for team-equivalence)
  3. Cross-tenant chunk shadow rejection: same sourcePath under two tenants yields distinct chunk ids; A's content cannot suppress B's via id collision
  4. Spoof-rejection — write-side: forged client tenantId in parsed-chunk-v1 payload → server-overwritten OR rejected per spoofRejectionMode; warning log emitted
  5. Spoof-rejection — read-side: forged client tenantId in query parameter → ignored (filter derived from server-side authenticated AgentIdentity only)
  6. Backup-record path isolation: manageDatabaseBackup({action: 'import'}) restore path bypasses ingestion validation by-design (preserves embeddings), but tenant boundary still respected via existing record metadata
  7. Search hydration tenant-aware: search results cite chunk.metadata.source (now tuple), don't claim file existence under server neoRootDir for non-local-tenant content (placeholder behavior pending Phase 2 Q12 hydration resolution; this AC asserts current SearchService respects tenant-tuple semantics)
  8. End-to-end via every public facade: ask_knowledge_base, query_documents, get_class_hierarchy, get_document_by_id, list_documents ALL respect tenant filter; no facade can bypass

Acceptance Criteria

  • QueryService.queryDocuments injects where: {tenantId: {...}} from authenticated AgentIdentity
  • SearchService query path inherits filter (since it consumes QueryService output)
  • Team-namespace constant ('neo-shared') documented in aiConfig + JSDoc
  • Application-layer filter Option A implemented; Chroma-layer hardening deferred to Phase 2-or-later per Q13b lean
  • All 8 fail-closed test cases pass
  • Tests parameterize over every public KB MCP facade (ask_knowledge_base, query_documents, get_class_hierarchy, get_document_by_id, list_documents)
  • Spec coverage report demonstrates no facade bypass

Out of Scope

  • Chroma-layer enforcement (Q13b Option B) → future Discussion if Option A leak class manifests
  • Hydration mode choice (Q12) → Phase 2D (this sub asserts current SearchService respects tenant-tuple; doesn't refactor hydration)
  • AgentIdentity context propagation through service stack → Phase 2 (this sub uses mocked AgentIdentity in tests)

Related

  • Parent: #11625
  • Blocked-by: Phase 0/1A (schemas), Phase 0/1B (registry), Phase 0/1C (write-side stamping)
  • Blocks: Phase 0/1 Epic closeout
  • Discussion source: #11623 §4 Q13b + §8 fail-closed test cases + §10 Graduation Criteria #13

Origin Session ID

7360e917-1733-4cdd-a6f3-5ac51c34b838

Handoff Retrieval Hints

  • QueryService.mjs:116-128 is the read-side surface
  • SearchService.mjs:95-100 is the search-result-consumer surface
  • MemoryService.mjs:391-410 is the Memory Core read-side filter pattern to mirror
  • Fail-closed test suite is load-bearing — author tests FIRST to define security boundary, then implement filter against them
tobiu referenced in commit 68eb22e - "docs(agentos): Phase 3A cloud-deployment guide scaffold (#11627) (#11668) on May 20, 2026, 7:59 AM
tobiu referenced in commit 93276d7 - "feat(kb): read-side tenant isolation filter + fail-closed suite (#11632) (#11674) on May 20, 2026, 11:03 AM
tobiu closed this issue on May 20, 2026, 11:03 AM