Skip to content

Drop the fail-open tenantId = "" default from Core retrieval APIs #20

Description

@wyckit

Deferred from PR #18. This is the mechanism that made that review's findings possible, and it stays armed for all future code until it's removed.

The problem

Roughly 50 Core APIs declare string tenantId = "" — across CognitiveIndex, KnowledgeGraph, ClusterManager, LifecycleEngine, AccretionScanner, MemoryDiffusionKernel, NamespaceRegistry, SpreadingActivationService.

"" is not a sentinel. It is the legacy partition: a real, readable, writable dataset.

So "I forgot to pass the tenant" is well-typed, compiles without a warning, and degrades to cross-tenant legacy scope rather than to an error. The compiler cannot help, and neither can review — the call site looks fine.

Evidence this actually fires

Two clean instances, both found in the PR #18 review:

SynthesisEngine — commit 4eb5386 added string tenantId = "" to the signature and threaded it into GetAllInNamespace, then dropped it one frame down into ChunkMemories(entries, ns), where ListClusters(ns) / GetCluster(id) bound silently to the legacy tenant. Result: another tenant's cluster label reached the model prompt.

DiffusionKernelWarmupService — identical shape. GetBasis(ns) defaults tenantId to "", so no tenant partition is ever warmed even though the sweep enumerates tenant-only namespace names.

Same failure, twice, independently. That's a defaulting problem, not a discipline problem.

Approach

Drop the = "" default from the retrieval-side Core APIs and make every call site name its tenant explicitly. Large but mechanical, and the compiler finds every site.

PR #18 already applied this rule to everything it added: ChunkMemories, GetClusterMembershipsForEntry, and all six DebateSessionManager session members take a required tenantId. This issue extends it to the existing surface.

Fold in: converge the bare-id resolvers

Four sites still open-code the "resolve a bare id and check access" predicate with three different resolution semantics:

  • GraphTools.DenyIfCannotWrite
  • LifecycleTools.Resolve
  • AdminTools.CanReadEndpoint
  • CoreMemoryTools.CanReadEntryById

All four are ACL-correct today, which is why PR #18 left them alone. Converting them to EntryAccessResolver changes legacy resolution semantics from "first match wins per the global id→ns map" to "unique match among visible namespaces" — a behaviour delta on the most common deployment, taken for consistency rather than for a live bug. It belongs here, behind its own tests, not in a security PR.

Worth noting while doing it: the comment at CoreMemoryTools.cs:78-79 justifying its duplicate helper is factually wrongCognitiveEntryInfo does carry Namespace (SearchResults.cs:33). That incorrect belief is what spawned the duplicates. Delete the comment along with the helper.

Acceptance criteria

  • = "" removed from retrieval-side Core APIs; call sites name their tenant.
  • The four resolvers above converge on EntryAccessResolver, with a test pinning the legacy resolution-semantics change.
  • CoreMemoryTools.cs:78-79 comment removed.
  • No behaviour change for single-agent/legacy deployments — the existing legacy-mirror tests must stay green unchanged.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions