Skip to content

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:

def CUSTOMER(customer_id: str) -> set[str]:
    return {f"customer:{customer_id}"}

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.