LearnNewsExamplesServices
Frontmatter
id13600
titlewho_is_online: tenant-scope the AgentIdentity roster read for multi-tenant cross-tenant isolation
stateOpen
labels
enhancementaiarchitectureneeds-re-triage
assigneesneo-opus-ada
createdAtJun 20, 2026, 8:03 AM
updatedAt3:21 PM
githubUrlhttps://github.com/neomjs/neo/issues/13600
authorneo-opus-ada
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]

who_is_online: tenant-scope the AgentIdentity roster read for multi-tenant cross-tenant isolation

Open Backlog/active-chunk-2 enhancementaiarchitectureneeds-re-triage
neo-opus-ada
neo-opus-ada commented on Jun 20, 2026, 8:03 AM

Context

who_is_online (WakeSubscriptionService) reports the online roster by reading ALL AgentIdentity nodes (_listAgentIdentityNodes, ai/services/memory-core/WakeSubscriptionService.mjs:595) plus each rostered agent's most-recent AGENT_MEMORY activity (_readActivityRecency:687). #13517/#13527 bound RequestContextService for the wake-subscription path but deferred the roster's tenant-scoping — documented inline as "tracked separately" (WakeSubscriptionService.mjs:673-674, :698-699) — without filing a tracking ticket. This files it (closing that loop).

The Problem

_listAgentIdentityNodes runs a raw SELECT data FROM Nodes WHERE json_extract(data,'$.label')='AgentIdentity' with no tenant-scoping. In a MULTI-TENANT cloud (multiple tenants sharing one graph), a caller in tenant A gets tenant B's agent identities + their turn-activity (online status) — a cross-tenant identity + activity leak. NOT live in the current single-deployment swarm, where all rostered agents are same-tenant, so this is a pre-multi-tenant-cloud hardening item, not a live incident.

The nuance — why the naive per-caller RLS fix is WRONG

A per-caller user_id RLS filter at the activity read is explicitly the wrong layer (documented at :669-672): each agent's raw AGENT_MEMORY is tagged with its OWN per-agent userId, so a per-caller filter would hide same-deployment teammates from each other — the roster IS the intended visibility boundary within a tenant. The correct fix is TENANT-scoping at the roster level: _listAgentIdentityNodes scopes to the requester's tenant, so a tenant sees its own full roster but not other tenants'.

Acceptance Criteria

  • _listAgentIdentityNodes scopes the AgentIdentity roster to the requester's tenant boundary (not per-caller userId).
  • Same-tenant teammates remain mutually visible (no regression of roster-within-tenant visibility).
  • Multi-tenant test: tenant A's who_is_online does NOT return tenant B's AgentIdentity nodes / activity; same-tenant teammates still appear.
  • The "tracked separately" comments (:673-674, :698-699) updated to cite this ticket.

Out of Scope

  • Single-deployment swarm behavior (all same-tenant) — unchanged.
  • Per-caller RLS on the activity read — the explicitly-wrong layer (breaks same-tenant roster visibility).

Evidence

WakeSubscriptionService.mjs:595 (unscoped roster SELECT), :669-676 + :691-699 (the documented deferral). Surfaced by a memory-core graph-read RLS audit (2026-06-20); the rest of the tool-exposed graph-read surface was confirmed RLS-sound in the same pass — transitionTask is gated by an originator/assignee authz check at MailboxService.mjs:1340 before its state read, and the other raw-.prepare() reads (#exportGraph, syncWorkspace, the @protected subscription loaders) are maintenance/internal, not caller-reachable cross-tenant.

Release classification

post-release / pre-multi-tenant-cloud security hardening. Boardless.

Origin Session ID: abe80be3-6235-4a9e-99bc-b14659ba806a