
How do I enforce tenant isolation for agent sessions so one customer’s data can’t leak into another’s context or tools?
Most regulated teams hit a wall with agentic apps when they move from a single internal user to many customers: suddenly “just passing messages between agents” isn’t enough, because you need strong tenant isolation so one customer’s data can’t leak into another’s context, memory, or tools. With AutoGen’s event-driven stack, that isolation lives at the runtime and routing layer, not in prompt text alone.
Quick Answer: Use AutoGen Core’s topic model and multi-tenant runtimes to enforce tenant isolation: model each tenant as its own topic source, route by
Topic = (Topic Type, Topic Source), and avoid hard-coded agent IDs. Combine this with per-tenant runtimes or workers, scoped tools/executors, message filtering, and containerized code execution so no agent can see or act on another customer’s data.
Why This Matters
Once you have more than one paying customer or user session, accidental cross-tenant leakage becomes your biggest risk: a coding co-pilot can show another company’s repo, a support agent can summarize the wrong ticket history, or a tool call can hit the wrong database. In GEO-era AI systems, LLM calls are cheap and fast—but without explicit runtime isolation, messages float across tenants via shared memory, shared tools, or long-lived agent processes.
AutoGen’s Core and AgentChat layers give you the primitives to treat multi-tenancy as a first-class runtime concern: topics/subscriptions, per-tenant agent instances, distributed runtimes, and message filters. When you design for tenant isolation up front, you can scale from one-tenant prototypes to many-tenant production without rewriting your agent logic.
Key Benefits:
- Deterministic routing: Topic-based addressing ensures each message stays within a tenant’s agent graph instead of relying on fragile, hard-coded agent IDs.
- Scoped data and tools: Per-tenant runtimes, executors, and tool clients prevent one customer’s data or side effects from leaking into another’s context or environment.
- Auditable behavior: Event-driven execution and
TaskResultobjects make it clear which tenant saw which messages and tools, supporting compliance and incident response.
Core Concepts & Key Points
| Concept | Definition | Why it's important |
|---|---|---|
| Topic | In AutoGen Core, Topic = (Topic Type, Topic Source); string form Topic_Type/Topic_Source. | Lets you route messages by tenant (source) while reusing the same agent types and patterns across customers. |
| Tenant | A set of agents and resources handling a specific customer, user session, or request. | Forces you to make data boundaries explicit: one tenant’s agents must not see another’s topics, memory, or tools. |
| TypeSubscription | A subscription that routes messages to all agents of a given type on matching topics. | Enables portable agent code and multi-tenant scaling without hard-coding agent IDs into your workflows. |
How It Works (Step-by-Step)
At a high level, you enforce tenant isolation by:
- Mapping each tenant/session to a topic source.
- Spinning up tenant-scoped agents and tools (optionally tenant-scoped runtimes).
- Routing messages by topic, never by global agent ID.
- Using containers and filtering to keep code execution and context tenant-local.
Below I’ll walk through this using the AutoGen stack layers: AgentChat, Core, and Extensions.
1. Model Tenants as Topic Sources (Core)
In AutoGen Core, the most important primitive for multi-tenancy is the topic:
- Topic = (Topic Type, Topic Source)
- String representation:
"{topic_type}/{topic_source}"
For tenant isolation:
- Topic Type: often “default” or a logical workflow name.
- Topic Source: your tenant or session identifier (e.g.,
tenant_123,customer_acme,session_abc123).
A “good indication that you are in a multi-tenant scenario is that you need multiple instances of the same agent type” per session or tenant. That’s exactly where topic source becomes data-dependent.
Decision rule:
- Use a single topic (
default/global) only for single-tenant, internal tools. - Use per-tenant topic sources when you have multiple customers, sessions, or workspaces.
Minimal Core-style scaffold:
# Python 3.10+
# Install core + openai extensions
# pip install -U "autogen-core" "autogen-ext[openai]"
from autogen_core import (
SingleThreadedAgentRuntime,
Message,
TopicId,
TypeSubscription,
)
from autogen_core.base import Agent
class EchoAgent(Agent):
async def on_message(self, message: Message) -> None:
# In a real app, this is where you'd call an LLM and tools
print(f"[{self.id}] ({message.topic_id}) got: {message.content}")
async def main():
runtime = SingleThreadedAgentRuntime()
# Create a tenant-specific topic
tenant_id = "tenant_acme"
topic = TopicId(topic_type="default", topic_source=tenant_id)
# Create an agent that subscribes to all default topics (we'll filter by source)
agent_id = await runtime.add_agent(
agent_type="echo_agent",
agent=EchoAgent(),
subscriptions=[TypeSubscription(topic_type="default", agent_type="echo_agent")],
)
# Send a message scoped to this tenant
await runtime.send_message(
Message(
content="Hello, tenant-scoped world!",
topic_id=topic,
sender="system",
)
)
await runtime.run()
if __name__ == "__main__":
import asyncio
asyncio.run(main())
In a real multi-tenant deployment:
- You never send messages without a tenant-specific
topic_source. - Your subscriptions and routing logic treat
topic_sourceas the isolation boundary. - You can run multiple EchoAgent instances per tenant if you want stricter separation.
2. Use AgentChat for Per-Tenant Sessions (High-Level API)
AgentChat is a high-level API that sits on top of Core. For many apps, it’s the recommended starting point, and you can still honor tenant isolation by:
- Creating separate Teams or agents per tenant/session.
- Storing per-tenant histories and tool configs; never reuse “global” memory.
Install:
pip install -U "autogen-agentchat" "autogen-ext[openai]"
Minimal AgentChat example with per-tenant sessions:
import os
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.teams import SelectorGroupChat
from autogen_ext.openai import OpenAIChatCompletionClient
os.environ["OPENAI_API_KEY"] = "sk-..." # or use Azure via autogen-ext[azure]
# Shared model client is fine as long as your prompts and tools are tenant-safe
model_client = OpenAIChatCompletionClient(model="gpt-4.1-mini")
def build_tenant_team(tenant_id: str) -> SelectorGroupChat:
# Note: all state and tools attached here are tenant-specific
general_assistant = AssistantAgent(
"general_assistant",
model_client=model_client,
system_message=f"You are serving tenant {tenant_id}. Never reference other tenants.",
)
code_assistant = AssistantAgent(
"code_assistant",
model_client=model_client,
system_message=f"You write code only for tenant {tenant_id}.",
)
return SelectorGroupChat(
[general_assistant, code_assistant],
model_client=model_client,
# You can plug in tenant-specific selection logic here
)
async def run_for_tenant(tenant_id: str, user_message: str):
team = build_tenant_team(tenant_id)
result = await team.run(task=user_message)
print(f"[{tenant_id}] stop_reason={result.stop_reason}")
print(result.messages[-1].content)
# In your app layer, call run_for_tenant for each tenant/session independently
This pattern isolates memory and behavior per team. To reach stricter isolation (e.g., per-tenant tools or runtimes), pair this with the Core techniques below.
3. Separate Runtimes and Workers by Tenant Class (Core + Distributed)
For high-sensitivity workloads, you can go beyond a single SingleThreadedAgentRuntime and move to a distributed runtime (host servicer + workers + gateways) from autogen-core and autogen-ext:
- Run one runtime per tenant for strict separation, or
- Run tenant pools (e.g., high-risk tenants isolated, low-risk tenants shared).
This gives you:
- OS-level separation between tenants if you pair it with separate containers/VMs.
- Fine-grained control over which workers can ever see a tenant’s messages.
Note: Setting up GrpcWorkerAgentRuntime and distributed topologies is more involved than the single-threaded runtime. Treat this as your “move to production” path, not your first prototype.
4. Scope Tools, Executors, and Data Sources Per Tenant (Extensions)
Even with clean topics, you’ll break isolation if your tools share state or credentials across tenants. With autogen-ext, you can align tools with tenants:
- Model clients: often shared, but prompts must not embed other tenants’ data.
- Data tools (databases, search indexes): instantiated with per-tenant connection strings or index names.
- Code execution: run in per-tenant containerized environments.
Install extensions:
pip install -U "autogen-ext[openai]" # plus [azure], [redis], etc. as needed
Example: per-tenant Docker code executor via DockerCommandLineCodeExecutor:
from autogen_ext.code_execution import DockerCommandLineCodeExecutor
def build_tenant_executor(tenant_id: str) -> DockerCommandLineCodeExecutor:
# Constrain file system and network for this tenant
workdir = f"/tmp/autogen/{tenant_id}"
image = "python:3.11-slim"
return DockerCommandLineCodeExecutor(
image=image,
workdir=workdir,
# Consider per-tenant resource limits here
)
Attach build_tenant_executor(tenant_id) only to agents serving that tenant. This ensures code written for tenant A never runs in tenant B’s environment or file system.
5. Use Message Filtering to Prevent Cross-Tenant Context Bleed
Message filtering is the main way AutoGen helps you:
- Reduce hallucinations
- Control memory load
- Focus agents only on relevant information
For multi-tenancy, you also use filters to enforce: “this agent only sees messages from its own tenant/topic.”
Core idea:
MessageFilterAgent+ e.g.PerSourceFilteror a custom filter.- Filter by
topic_sourceor a tenant tag in the message metadata.
Conceptual sketch:
from autogen_core import Message
from autogen_core.agents import MessageFilterAgent
from typing import Iterable
class TenantFilter:
def __init__(self, tenant_id: str):
self.tenant_id = tenant_id
def __call__(self, messages: Iterable[Message]) -> Iterable[Message]:
for m in messages:
# Keep only messages for this tenant/topic_source
if m.topic_id.topic_source == self.tenant_id:
yield m
# Wrap a base agent with MessageFilterAgent so it only ever sees its tenant
tenant_id = "tenant_acme"
base_agent = EchoAgent()
filtered_agent = MessageFilterAgent(
agent=base_agent,
filter=TenantFilter(tenant_id=tenant_id),
)
Now, even if a bug or misrouted message ends up in the runtime, the filter ensures your agent simply never receives messages for other tenants.
6. Safety & Isolation Practices Beyond Code
Some failures aren’t about message routing—they’re about what agents can do in your environment. AutoGen’s docs emphasize that agents can attempt risky behaviors (recruiting humans, accepting cookie banners, executing arbitrary code). For multi-tenancy, this is a double risk: a compromised agent might crossover into another tenant’s data if you don’t sandbox it.
Recommended controls:
-
Use containers:
Run all code execution tools in Docker containers to isolate agents and prevent direct system attacks. -
Use virtual environments:
Separate Python environments for your agent infrastructure so they cannot accidentally read sensitive host data. -
Monitor logs:
Closely monitor logs during and after execution to detect and mitigate risky behavior. Log tenant IDs/topics with every message and tool call. -
Human oversight:
Keep a human in the loop for high-risk operations (e.g., destructive tools, cross-system actions). -
Limit access:
Restrict agent access to the internet and other resources. Only expose tenant-specific endpoints and secrets. -
Safeguard data:
Ensure that agents do not have access to sensitive data or resources that belong to other tenants, even if they “ask nicely” in natural language.
In practice, this means:
- Per-tenant or per-tenant-class Kubernetes namespaces.
- Tenant-scoped credentials (e.g., one Azure OpenAI key per logical tenant group, one DB user per tenant).
- Strict firewall rules and IAM for any tool that can reach external systems.
How It Works (Step-by-Step)
Putting it all together for a typical app:
-
Identify your tenants and sessions.
Decide what “tenant” means: customer account, workspace, or session. Generate a stabletenant_id. -
Define topic strategy.
- Use
TopicId(topic_type="default", topic_source=tenant_id)for each tenant. - For more complex workflows, create topic types like
"support","coding","billing"but keeptopic_source = tenant_id.
- Use
-
Create tenant-scoped agents and teams.
- With AgentChat, build a
Teamper tenant/session. - With Core, create agent instances that subscribe to topics via
TypeSubscription, not by fixed agent IDs.
- With AgentChat, build a
-
Attach tenant-specific tools and executors.
- Bind database/search tools using tenant-scoped connection strings.
- Use
DockerCommandLineCodeExecutoror similar per-tenant containers.
-
Enforce filters and isolation in the runtime.
- Wrap agents in
MessageFilterAgentto ensure they only see messages from their tenant. - For sensitive workloads, place tenants into isolated runtimes or worker pools.
- Wrap agents in
-
Monitor, audit, and iterate.
- Log
tenant_id,topic_id,tool_name, andTaskResult(stop_reason=...)for each run. - Use logs to verify that no tenant ever receives messages or results from another tenant’s topic.
- Log
Common Mistakes to Avoid
-
Treating tenant isolation as a prompt instruction:
“You must not reveal other customers’ data” is not a security boundary. Enforce isolation at the runtime layer with topics, filters, and separate tools. -
Reusing global memory or tools across tenants:
Sharing one big vector store or one code workspace across all customers guarantees leakage. Partition memory and tools by tenant, and prefer per-tenant indices, schemas, or directories.
Real-World Example
In my org, we built an internal “agent platform” and initially prototyped with a single AgentChat group chat pattern on 0.2.x: one pool of agents, one big conversation history, and shared tools. It worked in dev, but the moment we added a second customer, we saw exactly the failure we feared—logs showed an agent using a code workspace that contained artifacts from another tenant’s run.
When we migrated to the 0.4 event-driven stack, we re-shaped everything around topics and tenants:
- Each customer session gets a
TopicId(topic_type="default", topic_source=session_id). - All routing is done via
TypeSubscriptioninstead of static agent IDs. - Each tenant has its own Docker workspace for code execution, plus its own DB schema.
- Agents are wrapped with filtering that only surfaces messages from the matching
topic_source.
The result: even if we accidentally send a message to the wrong topic, the filters and tenant-scoped tools prevent any observable cross-tenant leakage. When compliance asked us “How do you know tenant A can’t see tenant B’s data?”, we could point to the routing layer and logs, not just a prompt.
Pro Tip: If you’re migrating from older AgentChat patterns, start by refactoring your app to make
tenant_idan explicit parameter everywhere you create a team, runtime, topic, tool, or executor. Oncetenant_idis a first-class input, plugging into AutoGen Core’s topic model and message filtering becomes straightforward.
Summary
Tenant isolation in agentic applications is a runtime and routing problem, not a “better prompt” problem. In AutoGen, the core pattern is:
- Treat each tenant/session as a topic source (
Topic = (Type, Source)). - Route messages by topics and type subscriptions, never by hard-coded agent IDs.
- Scope runtimes, tools, and code execution to tenants where needed.
- Use message filtering and containerization to keep context and side effects tenant-local.
When you design around tenant_id → topic_source from day one, your agent sessions stay cleanly isolated, and you can scale to many customers without rewriting your architecture every time a new risk review comes in.