profile: field keys memory and session state per tenant on every turn, so one gateway multiplexes many tenants and their conversation histories never cross.
gateway.yaml
Quick Start
1
Bind two tenants to the same agent
Give each route a different
profile: value. The same support agent serves both tenants, but each route names its own memory namespace.2
Read the resolved profile in code
resolve_route() copies the winning binding’s profile onto the RouteMatch, so the gateway knows which tenant namespace to enter for the turn.3
Isolate a whole tenant server in one line
Bind A channel-level route also covers the threads beneath it — see Parent-chain matching.
profile: on a guild_id rule to isolate a whole tenant server — every current and future channel in the guild inherits the namespace.4
Leave a route unscoped
Omit
profile: (or leave it blank) and the route stays unscoped — match.profile is None and its memory is never namespaced.How It Works
resolve_route() picks the most-specific matching binding and copies its profile onto the returned RouteMatch; the gateway then enters that namespace before the agent runs, and every storage key for the turn is prefixed with profile:<name>:. An unmatched route yields RouteMatch(profile=None) and stays unscoped.
What the Profile Isolates Today
Only memory/session state is namespaced by the profile today; the other dimensions named on the routing protocol are separable follow-ups.Memory / session state
Shipped (PR #4343). Every storage key for the turn is prefixed with
profile:<name>:, so two routes multiplexed on one process never share a transcript.Secrets / model / instructions
Still follow-up. These dimensions are not yet consumed from the profile. Do not rely on the profile to scope secrets, override a model, or swap instructions.
Storage-key example
The tenant name appears asprofile:<name>: at the head of the key, so you can reason about exactly what is isolated.
When To Use a Profile
Fail-Closed Contract
Memory/session isolation shipped in PR #4343: the profile now keys memory per tenant on every routed turn. Secret-scope, model-override, and instruction-override remain separable follow-ups — the profile does not yet touch them.
Each concurrent turn keeps its own tenant scope for the duration of the turn and clears it afterward, so overlapping routes never share a namespace and a stale scope never leaks into a later turn.
Configuration Options
At runtime the resolved profile flows into
BotSessionManager’s storage-key derivation on every routed turn — Discord and Slack via _routed_message_handler, Telegram via handle_message — so the memory namespace is applied before the agent runs.
Best Practices
Give every tenant its own profile name
Give every tenant its own profile name
Use one stable, tenant-specific value per route (
acme, globex). Never reuse a profile name across tenants — that would merge their conversation histories.Bind tenants on stable identifiers
Bind tenants on stable identifiers
Route tenant profiles on
channel_id or account — stable platform ids — rather than display names, which change.Keep profile names out of user-controlled input
Keep profile names out of user-controlled input
Profile names come from your config, not from message content. Never derive a profile from a field a user can set.
Verify isolation by checking a storage key
Verify isolation by checking a storage key
Confirm a tenant is isolated by inspecting the head of its storage key — the tenant name must appear as
profile:<name>: before the base id (e.g. profile:acme:u1). An unscoped route shows no prefix (u1).Related
Route Bindings
The full routing surface — match by peer, role, channel, account, and priority.
Session Persistence
How profile-scoped routes persist under a namespaced session key across restarts.
Scoped Approvals
Durable, agent-scoped approval grants that don’t leak across agents.

