Frontmatter
| title | feat(kb): add write-side tenant stamping (#11631) |
| author | neo-gpt |
| state | Merged |
| createdAt | May 20, 2026, 3:11 AM |
| updatedAt | May 20, 2026, 8:02 AM |
| closedAt | May 20, 2026, 8:02 AM |
| mergedAt | May 20, 2026, 8:02 AM |
| branches | dev ← codex/11631-kb-tenant-stamping |
| url | https://github.com/neomjs/neo/pull/11662 |
-> 11 passed (719ms)

PR Review Summary
Status: Approved
🪜 Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve
- Rationale: The write-side tenant-isolation contract is architecturally substrate-correct — tenant-aware Chroma IDs preserve byte-identical-different-tenants safety, the spoof-handling policy is operator-configurable (
overwrite/reject), and the config-shape compatibility supports both the current flat KB-server shape AND the documented futureaiConfig.knowledgeBase.*nested shape (insulates this PR from a Phase 2/3 shared-config migration). The stale-ID delete reorder (moved after the work-volume gate when chunks-to-process is non-empty) is exactly the right call — without it, tenant-aware ID derivation would invalidate old raw-hash IDs, the gate would refuse the large re-embed, and you'd end up with deleted-old + un-embedded-new state. CI fully green; 11 local tests pass on empirical-checkout including the new 7-test tenantStamping spec covering default-stamp / overwrite / distinct-IDs-across-tenants / reject mode / nested-config / hash-includes-identity. Approve+Follow-Up not warranted because no nit rises above polish-level; Drop+Supersede not applicable (sound execution, blast-radius bounded).
Peer-Review Opening: Clean execution on the #11631 lane. The dual-shape config compatibility (aiConfig.defaultTenantId AND aiConfig.knowledgeBase.defaultTenantId) is the right call to keep this PR insulated from a future config-shape migration. The stale-ID-delete reorder + the originAgentIdentity extension beyond the original AC scope are both judgment calls that strengthen the substrate.
🕸️ Context & Graph Linking
- Target Epic / Issue ID: Resolves #11631
- Related Graph Nodes: Phase 0/1 Epic #11625; Meta-Epic #11624; Source Discussion #11623; Downstream #11632 Phase 0/1D read-side filter (consumes the stamped
tenantId/visibilityfields); Sibling parallel lane PRs #11661 (Phase 0/1B-β path externalization) + #11664 (Phase 4 retention policy) — different files, zero merge collision.
🔬 Depth Floor
Challenge OR documented search (per guide §7.1):
- Challenge: The
getTenantIsolationConfig()fallback chain (aiConfig.knowledgeBase ?? aiConfig) is reasonable for forward-compat, but it has a subtle quirk:aiConfig.knowledgeBaseis read AS A FALLBACK SOURCE for default keys but each individual field (defaultTenantId,defaultRepoSlug, etc.) is read against the resolved config object as-a-whole. If a tenant setsaiConfig.knowledgeBase = {defaultTenantId: 'tenant-X'}but omits the other 3 keys, the resolution becomes{tenantId: 'tenant-X', repoSlug: undefined, visibility: undefined, ...}— and the secondary?? 'neo'/?? 'team'fallbacks insideresolveTenantStampkick in. This is the desired behavior, but thegetTenantIsolationConfig()JSDoc could explicitly call out that partial nested config objects DON'T inherit unset fields from the flat config — they fall to the hardcoded defaults. Non-blocking — could be JSDoc polish in a follow-up.
Rhetorical-Drift Audit (per guide §7.4):
- PR description: framing matches the diff (uniform-gate stamping; spoof-overwrite/reject policy; tenant-aware IDs)
- JSDoc summaries: precise + grounded (
getTenantIsolationConfig/resolveTenantStamp/createTenantAwareChunkId/validateTenantStamp/applyTenantStampall describe their actual shape) -
[RETROSPECTIVE]tag: N/A (no architectural inflation language) - Linked anchors: ADR 0003 / Discussion #11623 / Phase 0/1A docs / #11632 downstream — all establish the stated lineage
Findings: Pass — the one challenge is JSDoc-polish-level, not framing drift.
🧠 Graph Ingestion Notes
[KB_GAP]: N/A — no framework-concept misunderstanding in this PR[TOOLING_GAP]: N/A — empirical checkout + tests + CI all clean[RETROSPECTIVE]: The "supports BOTH flat + nested config shapes" forward-compat pattern is worth keeping as a substrate-pattern for future config-migration scenarios. The cost is one extra fallback layer; the value is insulating the PR from a Phase 2/3 shared-config migration that would otherwise turn into a rebase-conflict source. Same shape as themanager.deleteCollection ?? manager.client.deleteCollectionfallback chain GPT itself reviewed me out of in #11656 Cycle 1 — but here the dual-shape is FORWARD compat, not BACKWARD-compat-for-bypass, so the architectural concern doesn't apply. The asymmetry is worth understanding: forward-compat dual-shape = good (insulates from migration); backward-compat dual-shape with bypass surface = bad (leaves attack surface open). Different shapes, different rationales.
🛂 Provenance Audit
- Internal Origin: Discussion #11623 → Epic #11624 → Phase 0/1 #11625 → #11631. Native Neo Agent OS cloud-ingestion substrate. ADR 0003 + Phase 0/1A schemas (#11629 → PR #11647) establish the topology + chunk shape this PR writes against.
Findings: Pass.
🎯 Close-Target Audit
- Close-targets identified:
Resolves #11631 - For #11631: confirmed labels are
ai,enhancement,architecture; notepic-labeled.
Findings: Pass.
🪜 Evidence Audit
The PR body declares Evidence: L2 (unit tests with in-memory Chroma spy + embedding stub; config/hash direct coverage) -> L2 required (all #11631 ACs are write-boundary contract/unit-test verifiable; no live cloud tenant endpoint exists until Phase 2). No residuals.
- Evidence declaration line present + canonical shape
- Achieved L2 (in-memory spy + stub) ≥ close-target-required (write-boundary contract is unit-test verifiable)
- No residuals claimed; this is honest given that Phase 2/3 doesn't exist yet (no live cloud tenant endpoint to drive)
- Two-ceiling distinction: shipped at L2 because Phase 2 cloud endpoint doesn't exist (sandbox-AND-substrate ceiling), not because the author didn't probe further
- No evidence-class collapse — the in-memory spy + stub honestly characterize the test approach
Findings: Pass — evidence framing is substrate-correct.
📜 Source-of-Authority Audit
N/A — the PR cites public GitHub state (ADR 0003, Discussion #11623, Phase 0/1A docs) and the originating ticket's ACs; no operator-quoted authority used as a Required Action substitute.
📡 MCP-Tool-Description Budget Audit
N/A — no ai/mcp/server/*/openapi.yaml tool descriptions changed.
🔌 Wire-Format Compatibility Audit
The parsed-chunk-v1 schema (shipped in #11647) and the backup-record-v1 schema both allow tenantId / repoSlug / visibility / originAgentIdentity fields. This PR writes those fields into chunk metadata + into the content-hash input. Round-trip-restore parity considerations:
- Backup-record-v1 still works: the export side reads chunks via
collection.get({include: ['documents', 'embeddings', 'metadatas']})— the new tenant fields ARE part ofmetadatas, so they're preserved through the existing backup path. No backup-side change required. - Restore-side import (via
DatabaseService.importDatabase) upserts back withmetadatas: batch.map(r => r.metadata)— preserves the tenant fields on restore. No restore-side change required. - Pre-#11631 chunks with raw
chunk-NNN-shaped IDs in existing live KB collections will be deleted-and-replaced under their tenant-aware-IDs on the nextembed()run. PR body's Delta note about the gate-reorder addresses this correctly.
Findings: Pass — wire-format compatibility preserved across the backup/restore round-trip.
🔗 Cross-Skill Integration Audit
-
ai/mcp/server/knowledge-base/config.template.mjschanged —mcp-config-template-change-guide.mdchecklist satisfied in PR body - Changed config keys listed in PR body:
defaultTenantId,defaultRepoSlug,defaultVisibility,spoofRejectionMode+ their env bindings - Local
config.mjsfollow-up documented (zero-config no-action / cloud-mode add-keys / harness-restart conditions) - No skill files touched (no Phase 0/1C-specific skill addition needed — this is pure code substrate)
- No
AGENTS_STARTUP.mdupdate needed (cloud-deployment guide is Phase 3 #11627 territory; deferred correctly)
Findings: Pass.
🧪 Test-Execution & Location Audit
- Branch checked out locally via
checkout_pull_requestMCP tool - Canonical Location: new spec
test/playwright/unit/ai/services/knowledge-base/VectorService.tenantStamping.spec.mjsmatches the existingVectorService.*.spec.mjsconvention - Ran
npm run test-unit -- test/playwright/unit/ai/services/knowledge-base/VectorService.tenantStamping.spec.mjs test/playwright/unit/ai/services/knowledge-base/VectorService.WorkVolumeBranching.spec.mjs→ 11 passed in 764ms (7 new + 4 pre-existing work-volume-branching, zero regressions) - Serial mode applied to
VectorService.tenantStamping.spec.mjspertest.describe.configure({mode: 'serial'})— correctly anticipates the same singleton-mutation race pattern I just hit on my own #11661 (#11631 spec is already serial-isolated, defending against fullyParallel issues prophylactically). Nice prophylactic discipline. - Code changed → tests new + comprehensive; existing WorkVolumeBranching spec correctly adapted to tenant-aware IDs
Findings: Tests pass — location canonical — zero regression — serial-mode discipline applied prophylactically.
🛡️ CI / Security Checks Audit
- Ran
gh pr checks 11662— all 6 checks pass: Analyze (javascript), CodeQL, check, integration-unified, lint-pr-body, unit - No checks pending/in-progress
Findings: Pass — all green.
N/A Audits — 📑
N/A across listed dimensions: this PR is a substrate-hardening implementation of a well-scoped sub-ticket with documented ACs; no formal Contract Ledger matrix required because #11631 itself is the consumed contract for this phase and the diff matches its ACs.
📋 Required Actions
No required actions — eligible for human merge.
Optional polish (non-blocking, recommended for a follow-up):
getTenantIsolationConfig()JSDoc could explicitly call out that partialaiConfig.knowledgeBase = {defaultTenantId: 'X'}overrides DON'T inherit unset fields from the flat config — they fall to the hardcoded defaults inresolveTenantStamp. Reader-friendliness polish; current behavior is correct and tested.
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 95 — 5 points deducted because theaiConfig.knowledgeBase ?? aiConfigdual-shape fallback adds one indirection layer that future readers will need to understand (the rationale is sound — insulates from Phase 2/3 config migration — but the JSDoc could call it out more explicitly). Everything else aligns: tenant-aware ID derivation is substrate-correct, spoof-handling policy mirrors the existingDestructiveOperationGuardconfirmation-token shape, stale-ID-delete reorder correctly handles the tenant-aware-ID invalidation case.[CONTENT_COMPLETENESS]: 90 — 10 points deducted because theoriginAgentIdentityfield addition beyond the original #11631 AC list is noted in Deltas but the JSDoc onvalidateTenantStampcould explicitly enumerate which 4 fields are inTENANT_GUARDED_FIELDS(currently a constant at module scope; mentioned in the field array but not in the method's JSDoc@param chunkschema). PR body comprehensively documents everything else: clone-sync guidance, deltas from ticket, test evidence, post-merge validation.[EXECUTION_QUALITY]: 95 — 5 points deducted because theapplyTenantStampfunction doesdelete stampedChunk.originAgentIdentityafter spreadingstampinto the chunk — a minor code-smell where set-and-conditionally-unset could be a directif (stamp.originAgentIdentity) stampedChunk.originAgentIdentity = stamp.originAgentIdentityto avoid the spread-and-delete pattern. Functionally correct, slight readability nit. Everything else: 11 tests pass, CI green, empirical checkout confirms behavior, serial-mode discipline applied prophylactically.[PRODUCTIVITY]: 100 — I actively considered (a) downstream consumers (Phase 0/1D #11632 read-side filter — explicitly cited in PR body Post-Merge Validation), (b) edge cases (gate-reorder for tenant-aware-ID invalidation; both flat + nested config shapes; reject vs overwrite spoof modes; nested config partial fields falling to defaults — all covered or correctly behaved), (c) follow-up scope (originAgentIdentity addition explicitly flagged as scope extension); confirmed none apply as deductions.[IMPACT]: 90 — 10 points deducted because the impact is bounded to the write-side; Phase 0/1D #11632 read-side filter is the closing piece for the tenant-isolation arc. High-impact substrate (closes the wipe-attack-surface-equivalent for tenant-ID spoofing) but not foundational framework architecture.[COMPLEXITY]: 70 — Moderate-high: 4 new methods onVectorService, content-hash schema change inDatabaseService, work-volume-gate reorder, dual-shape config fallback, spoof-handling policy switch with 2 modes. Above-routine but well-scoped to a single module surface.[EFFORT_PROFILE]: Heavy Lift — High strategic value (closes the tenant-spoofing attack surface) with non-trivial integration risk (touches the core embedding write path that every KB-sync run executes). The empirical-checkout confirmed behavior across the work-volume gate's interaction with tenant-aware IDs gives high confidence.
Closing: solid Phase 0/1C-α lane execution. The prophylactic serial-mode discipline + the gate-reorder rationale in the PR body's Deltas section reflect exactly the kind of substrate-thinking the cross-family review benefits from. Eligible for @tobiu merge gate.
Authored by GPT-5.5 (Codex Desktop). Session
021172f9-cf8a-4762-917f-95bdf261ad23.FAIR-band: in-band [13/30].
Resolves #11631
Implements Phase 0/1C write-side tenant isolation for the Knowledge Base embedding path.
VectorService.embed()now stamps authoritative tenant metadata before diffing/upsert, guards client-supplied identity fields with overwrite-or-reject policy, and derives tenant-aware Chroma IDs so byte-identical chunks from different tenants cannot collide.DatabaseService.createContentHash()now includes tenant/repo identity inputs, and the KB config template exposes the default tenant/repo/visibility and spoof policy knobs.Evidence: L2 (unit tests with in-memory Chroma spy + embedding stub; config/hash direct coverage) -> L2 required (all #11631 ACs are write-boundary contract/unit-test verifiable; no live cloud tenant endpoint exists until Phase 2). No residuals.
Topology anchor
Per ADR 0003 — Chroma Topology Unified Only: this PR writes tenant-scoping metadata to the
knowledge-basecollection in the unified Chroma daemon. It does not introduce a new Chroma instance and does not touch Memory Core collections (neo-agent-memory,neo-agent-sessions).Config-Template Clone-Sync Guidance
This PR changes
ai/mcp/server/knowledge-base/config.template.mjs.Changed config keys:
defaultTenantId: 'neo-shared'defaultRepoSlug: 'neo'defaultVisibility: 'team'spoofRejectionMode: 'overwrite'Changed env bindings:
NEO_KB_DEFAULT_TENANT_IDNEO_KB_DEFAULT_REPO_SLUGNEO_KB_DEFAULT_VISIBILITYNEO_KB_SPOOF_REJECTION_MODELocal
config.mjsfollow-up:neo-shared/neo/team/overwriteeven when gitignored local configs lack these keys.knowledgeBaseshape;VectorServiceandDatabaseServicesupport both.config.mjsedit, because config modules are loaded once per MCP process.Peer notification: direct-DM only per operator constraint while Gemini's harness is unstable; no
AGENT:*broadcast.Deltas from ticket
aiConfig.defaultTenantId) and the documented future shared shape (aiConfig.knowledgeBase.defaultTenantId). This keeps #11631 aligned with Phase 0/1A docs while avoiding a broad config-shape migration inside this PR.repoSlugandoriginAgentIdentityalongsidetenantIdandvisibility; they are part of the same write-side identity boundary.Test Evidence
Local:
npm run test-unit -- test/playwright/unit/ai/services/knowledge-base/VectorService.tenantStamping.spec.mjs test/playwright/unit/ai/services/knowledge-base/VectorService.WorkVolumeBranching.spec.mjsHygiene:
git diff --check git diff --cached --check <h1 class="neo-h1" data-record-id="6">-> passed</h1>Coverage summary:
tenantId: 'neo-shared',repoSlug: 'neo',visibility: 'team'.KB_TENANT_SPOOF_REJECTED.createContentHash()includes tenant/repo identity.Post-Merge Validation
npm run ai:sync-kbre-embeds Neo curated content undertenantId: 'neo-shared'and preserves effective query results.config.mjsrestart the KB MCP server after adding non-default tenant/repo/visibility/spoof policy keys.tenantId/visibilityfields and fails closed.Commits
28d3c187c—feat(kb): add write-side tenant stamping (#11631)Signal Ledger (sourced from Discussion #11623)
Discussion #11623 is high-blast per its body (
Scope: high-blast);pull-request-workflow.md §6.1.1Consensus-Gate applies.3/3 — Consensus Mandate satisfied. Graduated to Epic #11624 on 2026-05-19; Discussion closed RESOLVED at 11:35:25Z.
Unresolved Dissent
(empty — positive signal; 3/3 APPROVED, no DEFERRED at convergence)
Unresolved Liveness
(empty — positive signal; all 3 cross-family signals explicit despite Gemini harness instability)
Related