Quality
Quality: Q4/5
Evidence: current-code call sequence, duplicate search, and deterministic scheduling sketch. This still needs an executable regression before it graduates to Q5.
Threat model: a configured multi-agent deployment using per-agent tenant routing. No external attacker is required; overlapping trusted agent activity can corrupt or disclose data across configured tenant boundaries.
Summary
Per-agent tenant routing is stored as mutable state on a single memoized LibravDBClient. Overlapping agent activity can change that state after one agent has scheduled work but before its RPC begins, causing the request to use another agent's tenant header or read-tenant list.
This can cross-wire both writes and reads between tenants.
Evidence
src/plugin-runtime.ts memoizes one shared LibravDBClient in createPluginRuntime() / ensureStarted().
src/index.ts resolves tenantIdByAgent in before_prompt_build, then mutates the shared client with setTenantKey() and setReadTenants().
src/libravdb-client.ts stores both values on instance fields. The transport interceptor reads self.tenantKey when each request starts; searchTextCollections() likewise reads and mutates the shared tenant fields during fan-out.
src/context-engine.ts deliberately queues afterTurn ingestion asynchronously. The queued task later calls runtime.getClient() and afterTurnKernel() without capturing the originating agent's routing context.
The comment above setTenantKey() calls this thread-safe because the interceptor reads the field for every request, but that only makes updates visible; it does not associate a tenant with the logical request that selected it.
Deterministic reproduction sketch
Configure:
{
"tenantIdByAgent": {
"agent-a": "tenant-a",
"agent-b": "tenant-b"
}
}
Then instrument the outgoing tenant header and force this schedule:
- Run Agent A's
before_prompt_build; the shared client is set to tenant-a.
- Queue an Agent A
afterTurn ingestion behind a deferred task; afterTurn returns before the RPC runs.
- Run Agent B's
before_prompt_build; the same client is changed to tenant-b.
- Release Agent A's queued task.
- Observe Agent A's
afterTurnKernel(sessionId=A) leave with the tenant-b header.
A regression can create two Agent A turns so the second waits behind a deferred first afterTurnKernel, dispatch Agent B's routing hook before releasing the first call, then assert the second Agent A call still carries tenant-a. Current behavior uses tenant-b.
The same class of race can affect overlapping assembly/search flows because tenant selection is hook-global rather than request-scoped.
Expected behavior
Tenant routing must be captured per logical agent operation and remain fixed for every RPC it starts. One agent's hook must never retarget another agent's queued or overlapping work.
Possible designs include request-scoped call options/headers, a tenant-bound client view, or clients keyed by resolved tenant/read-access configuration. Temporarily mutating a singleton client is unsafe.
Impact
- Agent A's transcript can be ingested into Agent B's isolated database.
- Agent B's memory can be retrieved and injected into Agent A's context.
- Multi-tenant data can be corrupted or disclosed across configured isolation boundaries.
Duplicate check
No issue or PR matching tenantIdByAgent, setTenantKey, shared/wrong tenant, or tenant-routing races was found. PR #364 is distinct: it preserves colliding result IDs across fan-out tenants and restores state in searchTextCollections, but does not make tenant routing request-scoped.
– Vale
Quality
Quality: Q4/5
Evidence: current-code call sequence, duplicate search, and deterministic scheduling sketch. This still needs an executable regression before it graduates to Q5.
Threat model: a configured multi-agent deployment using per-agent tenant routing. No external attacker is required; overlapping trusted agent activity can corrupt or disclose data across configured tenant boundaries.
Summary
Per-agent tenant routing is stored as mutable state on a single memoized
LibravDBClient. Overlapping agent activity can change that state after one agent has scheduled work but before its RPC begins, causing the request to use another agent's tenant header or read-tenant list.This can cross-wire both writes and reads between tenants.
Evidence
src/plugin-runtime.tsmemoizes one sharedLibravDBClientincreatePluginRuntime()/ensureStarted().src/index.tsresolvestenantIdByAgentinbefore_prompt_build, then mutates the shared client withsetTenantKey()andsetReadTenants().src/libravdb-client.tsstores both values on instance fields. The transport interceptor readsself.tenantKeywhen each request starts;searchTextCollections()likewise reads and mutates the shared tenant fields during fan-out.src/context-engine.tsdeliberately queuesafterTurningestion asynchronously. The queued task later callsruntime.getClient()andafterTurnKernel()without capturing the originating agent's routing context.The comment above
setTenantKey()calls this thread-safe because the interceptor reads the field for every request, but that only makes updates visible; it does not associate a tenant with the logical request that selected it.Deterministic reproduction sketch
Configure:
{ "tenantIdByAgent": { "agent-a": "tenant-a", "agent-b": "tenant-b" } }Then instrument the outgoing tenant header and force this schedule:
before_prompt_build; the shared client is set totenant-a.afterTurningestion behind a deferred task;afterTurnreturns before the RPC runs.before_prompt_build; the same client is changed totenant-b.afterTurnKernel(sessionId=A)leave with thetenant-bheader.A regression can create two Agent A turns so the second waits behind a deferred first
afterTurnKernel, dispatch Agent B's routing hook before releasing the first call, then assert the second Agent A call still carriestenant-a. Current behavior usestenant-b.The same class of race can affect overlapping assembly/search flows because tenant selection is hook-global rather than request-scoped.
Expected behavior
Tenant routing must be captured per logical agent operation and remain fixed for every RPC it starts. One agent's hook must never retarget another agent's queued or overlapping work.
Possible designs include request-scoped call options/headers, a tenant-bound client view, or clients keyed by resolved tenant/read-access configuration. Temporarily mutating a singleton client is unsafe.
Impact
Duplicate check
No issue or PR matching
tenantIdByAgent,setTenantKey, shared/wrong tenant, or tenant-routing races was found. PR #364 is distinct: it preserves colliding result IDs across fan-out tenants and restores state insearchTextCollections, but does not make tenant routing request-scoped.– Vale