Multi-Tenant Isolation¶
When a single agent serves multiple users, customers, or tenants, data belonging to one must not influence output for another. Strahl enforces this at the tool call level using visibility tags that are resolved from the actual tool arguments at analysis time.
Per-customer tag pattern¶
Use a function that returns a customer-scoped tag set:
Apply it symmetrically — to the label on the customer's data, and to the visibility check on any tool that writes to that customer.
Example: support agent¶
A support agent can look up and reply to any customer, but customer A's data must not influence a reply to customer B.
import strahl
from strahl import Label
def CUSTOMER(customer_id: str) -> set[str]:
return {f"customer:{customer_id}"}
@strahl.tool(
requires=Label(
source={"support-agent"},
visibility=lambda customer_id: CUSTOMER(customer_id),
),
produces=Label(
source={"crm"},
visibility=lambda customer_id: CUSTOMER(customer_id) | {"support-agent"},
),
)
def lookup_customer(customer_id: str) -> str:
...
@strahl.tool(
requires=Label(
source={"support-agent"},
visibility=lambda customer_id: CUSTOMER(customer_id),
),
produces=Label(
source={"support-agent"},
visibility=lambda customer_id: CUSTOMER(customer_id),
),
)
def reply_to_customer(customer_id: str, message: str) -> None:
...
If the model calls reply_to_customer(customer_id="B", message="...") but the message argument was influenced by content labeled visibility={"customer:A"}, the call is denied — "customer:A" is not in "customer:B"'s visibility set.
Per-tenant client instances¶
For hard tenant isolation at the SDK level, use a Strahl instance per tenant rather than the global default client:
from strahl import Strahl, Label
def make_agent(tenant_id: str) -> Strahl:
agent = Strahl.from_default() # inherits registered tools
agent.set_role_labels({
"user": Label(source={f"tenant:{tenant_id}"}, visibility={f"tenant:{tenant_id}"}),
"assistant": Label(source={"assistant"}, visibility={f"tenant:{tenant_id}"}),
})
return agent
This ensures role labels — which apply to every message in the transcript — are scoped to the tenant making the request.
Labeling tenant documents¶
When loading per-tenant context (user files, account records, previous conversations), register them as documents with a tenant-scoped label:
agent.add_document(
f"account-{tenant_id}",
account_data,
label=Label(
source={f"tenant:{tenant_id}"},
visibility={f"tenant:{tenant_id}"},
),
)
This ensures the content from tenant A's account data cannot influence tool calls in tenant B's session, even if both agents share the same tool registrations.