Session Isolation: Keeping One User's Agent State Separate from Another's
Cross-session leaks are security incidents, not "memory bugs."
This cheatsheet scopes short-term agent state so tenant A never reads tenant B's history, scratchpad, or tool cache.
How to Use This List
- Apply during design review and again before production.
- Tick only what is true in running code and storage, not in slides.
- Fail closed: if authz is uncertain, do not load session state.
- Re-audit after new channels (Slack, email, widgets) attach to the same agent.
A - Identity and Keys
| Check | Rule | Failure mode if skipped |
|---|---|---|
| 1. Authenticated principal | Every turn resolves tenant_id + user_id (or service account) from verified auth, not from prompt text | Spoofed identity via message body |
| 2. Composite store keys | Session keys include tenant (and user) scope: agent:stm:{tenant}:{user}:{session} | UUID-only keys collide or get guessed across tenants |
| 3. Client session ids | Treat client-supplied session ids as untrusted labels; bind them to the principal server-side | IDOR: open another user's session by id |
| 4. No global singleton memory | Ban process-global HISTORY = [] in multi-tenant hosts | Last request wins; silent cross-talk |
B - Read and Write Paths
| Check | Rule | Failure mode if skipped |
|---|---|---|
| 5. Authorize before GET | On load, verify the principal may access that session row/key | Horizontal privilege escalation |
| 6. Authorize before SET/DELETE | Same check on write and clear-chat | Attacker wipes or poisons others' state |
| 7. Tool cache isolation | Cache keys include tenant/user/session (or stronger); never key only by tool args | Shared cache returns other tenants' documents |
| 8. Scratchpad isolation | Pads live under the same session scope as messages | Progress state leaks across users |
C - Runtime and Workers
| Check | Rule | Failure mode if skipped |
|---|---|---|
| 9. No ambient session in thread locals without clear | If you use contextvars/thread locals, set and clear per request | Worker reuse serves wrong session |
| 10. Queue jobs carry scope | Async workers receive tenant/user/session in the job payload and re-check authz | Background tool run attaches to wrong chat |
| 11. Log redaction | Logs use session ids, not full prompts by default; never log foreign tenant data in shared traces without ACL | Ops tools become an exfiltration path |
| 12. Multi-agent handoffs | Handoff packages stay inside the same tenant boundary; strip foreign keys | Specialist agent loads another org's pad |
D - Lifecycle and Compliance
| Check | Rule | Failure mode if skipped |
|---|---|---|
| 13. TTL per session class | Abandoned chats expire; legal holds are explicit, not accidental forever keys | Redis/disk fills; retained PII without purpose |
| 14. User clear / account delete | Product paths delete session keys and related caches | "Delete my data" leaves agent STM |
| 15. Admin support access | Break-glass access is audited and time-bounded | Silent cross-tenant reads by staff tools |
Quick Key Layout Reference
agent:stm:{tenant_id}:{user_id}:{session_id} # messages + scratchpad blob
agent:tcache:{tenant_id}:{user_id}:{session_id}:{tool}:{args_hash}
agent:lock:{tenant_id}:{user_id}:{session_id} # optional write lockdef assert_session_access(auth: dict, tenant_id: str, user_id: str, session_id: str) -> None:
if auth["tenant_id"] != tenant_id or auth["user_id"] != user_id:
raise PermissionError("session scope mismatch")
# Also verify session_id belongs to user in DB/index when ids are enumerableAnti-Patterns (Do Not Ship)
- Looking up sessions with only
session_idfrom the URL while trusting the client. - Keying tool cache as
sha256(query)globally "for hit rate." - Reusing one Redis DB without key prefixes across staging and prod.
- Putting tenant id only in the system prompt and not in the storage key.
- Sharing in-memory LRU of "recent sessions" across tenants on one box without scoped keys.
FAQs
Is a random UUID session id enough isolation?
No. UUIDs reduce guessing but do not replace authorization. Always bind session rows to tenant and user and check on every read/write.
Can two users share one session on purpose?
Yes for collaborative agents - model it as an explicit ACL (session members), not as a missing tenant check.
Where do API keys for tools live?
In secret storage per tenant or per integration install - not inside the shared session JSON when avoidable.
How do I test isolation?
Automated tests: user A writes a distinctive secret into session state; user B attempts read by id and by cache key; assert deny. Repeat after deploys.
Do vector stores need the same rules?
Yes for long-term memory. This page focuses on short-term session state; apply the same tenant filters on retrieval queries.
What about single-tenant self-hosted agents?
Still separate users and sessions if multiple people share the deployment. Isolation complexity scales with shared infrastructure.
Should summarization run in a shared worker pool?
Yes, but the job must carry scope and the summarizer must only read/write that session key.
How does this relate to prompt injection?
Injection can try to make the model request other sessions' data; isolation must be enforced in code so tool/runtime denies cross-scope access even if the model asks.
Related
- Redis for Fast Short-Term Agent State - key layout and TTL
- Short-Term Memory Basics - composite keys in a demo store
- Short-Term Memory Best Practices - production habits including isolation
- Short-Term Memory: What an Agent Actually Needs Within One Run - what lives in a session
- Agent Security Basics - broader security baseline
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.