Skip to main content
Version: 0.12.0

Memory tenancy and isolation

Introduced 0.12.0

Lucenia's inference memory layer is multi-tenant by identity. Every memory that is remembered, and every recall that reads one back, is bound to the organization and user of the verified caller — not to anything in the request body. An organization is the hard isolation boundary; a user is a partition within it. One cluster can therefore serve many organizations from one memory API, and no organization can see, recall, or forget another's memories.

This page is the security model in plain terms, for an operator standing up memory for an enterprise and for a compliance reader who needs to know exactly where the boundary is and how it is enforced. It covers what the isolation guarantees are; the companion pages cover how to turn tenancy on for your identity provider (Enterprise authentication for memory) and the day-one admin steps (Enable and configure memory).

tip

The short version: the caller's organization and user come from the authenticated token, the memory verbs pin them for you, and a request that tries to assert a different organization or user is rejected with 403. Each organization's memory — long-term, working, and drift anchors — lives in its own physical set of indices.


Table of contents


Two dimensions: organization and user

Tenancy has exactly two identity-bound dimensions:

DimensionWhat it isWhere it maps
Organization (org)The isolation boundary — a customer, a tenant, a business unit. Every organization gets its own physical set of memory indices.Surfaces as the tenant dimension of a memory scope.
UserThe individual identity within an organization.Surfaces as the user dimension of a scope.

Both are resolved from the verified identity on every call and pinned into the scope the verbs actually use. The other scope dimensions — namespace, agent, and session — remain free for the caller to use as sub-partitions within their own organization and user. So a caller can organize their own memories by session or agent however they like, but they can never widen the scope past their own organization and user.

note

Because the organization and user are taken from the token, the classic multi-tenant mistake — a client passing "tenant": "some-other-org" to read another tenant's data — cannot happen here. The layer overwrites those two dimensions with the verified identity before it touches the index, and rejects a request that asserted a conflicting value.

Where the identity comes from

When the security plugin authenticates a request, it attaches the verified user (name, backend roles, and identity attributes) to the request context. The memory layer reads the organization from that verified identity in a fixed order:

  1. A named identity attribute — by default the JWT claim org, surfaced as the attribute attr.jwt.org. This is controlled by plugins.memory.tenancy.org_attribute.
  2. A prefixed backend role — if no attribute is present and plugins.memory.tenancy.org_backend_role_prefix is set (for example org:), the layer scans the caller's backend roles for one that starts with the prefix and takes the remainder as the organization (so the backend role org:acme yields the organization acme). Disabled by default.
  3. The fallback — if neither yields an organization:
    • if plugins.memory.tenancy.require_org is true, the request is rejected with 403 (fail-closed); otherwise
    • the caller is placed in the organization named default.

The resolved organization name is normalized (lowercased) and validated against the pattern [a-z0-9][a-z0-9-]{0,62}; an identity carrying an organization value that cannot be normalized to that pattern is rejected with 403 rather than silently redirected.

The user dimension is always the authenticated user's name. There is no configuration for it — it is not client-supplied.

note

When the security plugin is disabled (or the caller is a cluster super-admin acting through the admin certificate), there is no verified identity to pin. In that mode the layer resolves to the default organization and an unset user. Multi-tenant isolation is therefore a property of running memory with security enabled — see Enterprise authentication for memory.

The exact settings, defaults, and per-identity-provider recipes live in Enterprise authentication for memory.

How enforcement works on every call

Every memory verb runs the same guard before it reads or writes:

  1. Read the verified identity from the request context (never the body).
  2. Resolve the caller's organization and user as above.
  3. Compare against any tenant/user the request tried to assert. If the request asserted a different organization or user, reject with 403.
  4. Build the effective scope: organization and user pinned from the identity; namespace, agent, and session copied from the request if present.
  5. Route the operation to that organization's physical index only.

Because step 5 selects per-organization indices, isolation is enforced at the storage layer, not just by a query filter. A recall in organization acme searches the acme indices and nothing else.

The per-organization physical indices

All three memory stores are physically partitioned per organization — one organization's data at rest never shares an index with another's. This is defense in depth beyond the scope filter, and the requirement for tenant-level data-at-rest separation under SOC 2, FedRAMP, and HIPAA.

StorePer-organization indexKeyed by
Long-term (curated, recallable)Alias .plugins-memory-longterm-<org> → concrete .plugins-memory-longterm-<org>__<model-slug>(org, model)
Working (rolling episodic turns).plugins-memory-working-<org>org
Drift anchors (goals, constraints, centroid).plugins-memory-anchors-<org>org
  • Long-term is fronted by a per-organization alias so its backing index can be re-pointed (a re-embed onto a new model) without moving live traffic. Because it stores the recall vectors, its concrete index is keyed by (org, model): the model slug is derived from the organization's embedding model, so an organization can change or add models and each gets its own self-consistent embedding space at its own vector dimension. See Enable and configure memory.
  • Working and anchors carry no model-specific vector, so each is keyed by organization alone.

Only two memory indices are shared cluster-wide, and both are non-tenant configuration registries, never memory content: the organization registry (.plugins-memory-orgs, one policy document per provisioned organization) and the per-user model-selection registry (.plugins-memory-users, one document per (org, user)). Every store that holds an organization's actual memories is physically partitioned per organization.

warning

All memory indices are protected system indices — they carry the .plugins-memory- prefix and are accessed by the verbs on your behalf under a stashed security context. You never read or write them directly, and end-user roles are not granted index permissions on them. See Enable and configure memory for the roles that gate the verbs.

An organization's long-term index is provisioned explicitly by an administrator with PUT /_plugins/_memory/org/<org> before that organization is used. Provisioning is covered in Enable and configure memory.

Single-org and multi-org deployments

The same build serves both shapes; the difference is entirely in how you source the organization.

Single-organization deployment. Every user shares one organization. The simplest form is to leave org sourcing at its defaults and let every authenticated caller fall into the default organization — memory then partitions only by user. Or provision one named organization and route all identities to it. Isolation between users still holds (each user's scope is pinned to their own name), but there is one set of physical indices.

Multi-organization deployment. Each customer, tenant, or business unit is a distinct organization. You issue tokens whose org claim (or backend role) names the organization, provision each organization once, and set plugins.memory.tenancy.require_org: true so that any identity without a resolvable organization is refused rather than pooled into default. Every organization's memories — long-term, working, and anchors — are in separate physical indices, and no verb can cross the boundary.

tip

For a compliance posture, set require_org: true. It makes the organization boundary fail-closed: an identity that cannot be attributed to a provisioned organization is denied service instead of quietly landing in a shared default bucket.

What a compliance reader should take away

  • The tenant of a memory is the identity of its author, cryptographically established at authentication time — not a value the client chose. The two boundary dimensions (organization, user) are non-spoofable from the request body.
  • A mismatch is a 403, not a silent reroute. A request that names a different organization or user than the token is refused.
  • Isolation is physical for every store. Long-term, working, and drift-anchor memory each live in a separate index per organization — not just a shared index with a tenant filter.
  • Fail-closed is one setting. require_org: true turns "no organization → shared default" into "no organization → denied."
  • The stores are not directly reachable. Memory indices are protected system indices; access is only ever through the audited verbs, gated by memory roles.