How ai‑memory's Per‑Operator Memory Slots Isolate Context Injection Between Shared‑Server Users
ai‑memory isolates per‑operator context through deterministic namespace derivation, read‑time SlotVisibility filtering, and write‑time SlotPlacement guards that prevent cross‑user slot injection.
ai‑memory provides a memory layer for AI‑augmented workflows, designed to run on shared servers where multiple operators might coexist. Its per‑operator memory slots feature ensures that one user cannot inject context into another user's session brief—a critical security boundary for multi‑tenant deployments. This article explains the three‑layer isolation mechanism implemented in the akitaonrails/ai-memory repository.
Namespace Derivation with IdentityKey
Every operator in ai‑memory is represented by an IdentityKey type. When converted to a storage path, it produces a deterministic, URL‑safe identifier through IdentityKey::path_segment().
In crates/ai-memory-core/src/slots.rs lines 15–21, the namespace is derived as follows:
IdentityKey::User("alice")→path_segment()→"u-alice"- Personal slots live at
"_slots/{namespace}/{filename}"
This design eliminates raw username injection. Because the segment is produced by a typed transformation rather than string concatenation, glob characters or path traversal sequences in usernames cannot create wildcard namespaces that match other operators' slots.
SlotVisibility: Read‑Time Filtering
The SlotVisibility enum controls which _slots/* pages a viewer may see. It is constructed via SlotVisibility::for_viewer(per_user, viewer) and evaluated through SlotVisibility::allows.
From crates/ai-memory-core/src/slots.rs lines 67–85:
- When
[slots] per_user = truein configuration, the viewer's namespace is extracted from theirIdentityKey - Shared slots (paths without an owner namespace, such as
"_slots/current-focus.md") are always allowed becauseslot_owner()returnsNone - Personal slots are allowed only if
slot_owner(path) == viewer_namespace
The session_brief_pages_with_slot_visibility function in crates/ai-memory-store/src/reader.rs lines 4392–4406 applies this filter to all briefing queries. This ensures that even if a malicious actor somehow guesses another user's slot path, the query layer drops it before the brief is assembled.
SlotPlacement: Write‑Time Protection
To prevent context injection—where one operator writes text that appears in another's brief—ai‑memory enforces namespace ownership on every upsert. The slot_placement function in crates/ai-memory-core/src/slots.rs lines 64–81 examines:
- The target slot path
- The writer's
IdentityKey(if provided)
It returns one of three outcomes:
| Outcome | Meaning |
|---|---|
OwnNamespace |
Writer owns this slot; write proceeds |
RewriteToOwn |
Redirect into writer's personal namespace |
ForeignNamespace |
Reject: writer cannot inject into another's slot |
This guard operates before any page content reaches storage. A shared‑server deployment can safely expose write endpoints because the engine itself prevents cross‑boundary injection.
Practical Isolation Examples
Creating a Personal Slot
use ai_memory_core::{IdentityKey, NewPage, PagePath, Tier};
use ai_memory_store::Store;
let store = Store::open("/tmp/ai-memory").unwrap();
let ws = store.writer.get_or_create_workspace("default".into()).await.unwrap();
let proj = store.writer.get_or_create_project(ws, "demo".into(), None).await.unwrap();
let alice = IdentityKey::User("alice".into());
let alice_slot = format!("_slots/{}/current-focus.md", alice.path_segment());
store.writer
.upsert_page(NewPage {
workspace_id: ws,
project_id: proj,
path: PagePath::new(&alice_slot).unwrap(),
title: "Alice’s focus".into(),
body: "Working on feature X".into(),
tier: Tier::Semantic,
frontmatter_json: serde_json::json!({}),
pinned: true,
links: vec![],
author_id: None,
expires_at: None,
entities: vec![],
})
.await
.unwrap();
This creates "_slots/u-alice/current-focus.md"—visible only to Alice when per_user isolation is enabled.
Querying with Visibility Constraints
use ai_memory_core::SlotVisibility;
let vis = SlotVisibility::for_viewer(true, Some(&alice));
let pages = store
.reader
.session_brief_pages_with_slot_visibility(ws, proj, 100, 100, vis)
.await
.unwrap()
.0;
// Contains Alice's slot, excludes all other personal slots
Blocked Cross‑User Write
let bob = IdentityKey::User("bob".into());
let bob_slot = format!("_slots/{}/current-focus.md", bob.path_segment());
let placement = ai_memory_core::slot_placement(&bob_slot, Some(&alice));
assert_eq!(placement, ai_memory_core::SlotPlacement::ForeignNamespace);
// Write rejected: Alice cannot inject context into Bob's namespace
Integration Across Surfaces
The same visibility rules apply consistently:
- HTTP layer:
crates/ai-memory-hooks/src/router.rslines 1502–1504 constructsSlotVisibilityfrom the authenticated request identity - MCP endpoint:
crates/ai-memory-mcp/src/server.rslines 2291–2294 builds the filter for remote Model Context Protocol clients - Storage tests:
crates/ai-memory-store/tests/slot_visibility.rsverifies that hostile viewer names cannot match foreign namespaces and that recent‑page pointers hide other operators' personal slots
Summary
- Deterministic namespaces from
IdentityKey::path_segment()prevent username‑based traversal attacks SlotVisibilityfilters reads at query time, dropping unauthorized personal slots from session briefsSlotPlacementblocks writes to foreign namespaces, eliminating context injection vectors- Consistent application across HTTP, MCP, and internal storage APIs ensures no leakage paths
Frequently Asked Questions
What happens if per_user is disabled in configuration?
When [slots] per_user = false, SlotVisibility::for_viewer returns SlotVisibility::All. All slots—both shared and personal—become visible to every viewer. This preserves backward compatibility for single‑user deployments but removes isolation guarantees.
Can an operator access another user's slot if they guess the exact path?
No. The SlotVisibility::allows check in reader.rs validates ownership against the viewer's derived namespace, not just the string path. Even with exact path knowledge, unauthorized slots are filtered from briefing results.
How does ai‑memory handle legacy shared slots?
Slots at paths like "_slots/current-focus.md" without an owner namespace are treated as shared. slot_owner() returns None for these, and SlotVisibility::allows always permits them regardless of viewer identity.
Where is the isolation mechanism tested?
The slot_visibility.rs test suite in crates/ai-memory-store/tests/ covers namespace collision resistance, cross‑operator visibility blocking, and recent‑page pointer sanitization.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →