Status: Reference implementation — extracted from a private internal GTM platform in production use; tenant data, provider adapters, and company-specific policy stay private. · Layer: Catalog and tenancy · Portfolio map ›
Keep tenant routing out of the model. Resolve the tenant before the agent runs, then hand it capabilities that expose content-level operations only — no tenant id, repo, namespace, or connection string for the model to choose or hallucinate.
agent-tenancy is a tenant-binding and capability-scoping layer for multi-tenant agent
runtimes. It generates a verified registry of each tenant's resource bindings — which
repo, which database, which channel, which credential — then turns those bindings into
tenant-scoped capabilities minted before an agent enters the loop.
The model never constructs a tenant id, a repository path, a namespace, an account URL,
or a connection string. It sees content-level operations like get("welcome");
deterministic code owns the resource routing.
acme = docs.for_tenant(tenant("acme", registry))
acme.get("welcome")
# No tenant, repository, namespace, or path selector is exposed on the handle.Deterministic tenant routing. Probabilistic content generation.
Generic agent tenancy
prompt ──▶ model picks tenant id / repo / namespace / creds ──▶ generic tools ──▶ shared infra
└─ symbolic id reasoning by a probabilistic model: hallucination + leakage risk
agent-tenancy
tenant resolved deterministically ──▶ scoped capabilities minted ──▶ model sees content-only ops
└─ the model is never a participant in tenant routing
This removes two recurring risks in multi-tenant agent systems:
- Misdirection — the model can't substitute or hallucinate another tenant's identifier through a scoped interface, because the interface has no identifier argument.
- Context leakage — tenant wiring doesn't have to be repeated through prompts and tool calls, so it can't drift or bleed across a long session.
Most multi-tenant systems assume deterministic application code constructs resource identifiers. Agent systems often do the opposite: they hand a probabilistic model the tenant ids, repo names, database namespaces, channel ids, and credential references as prompt text and tool arguments, and ask it to plug them into calls at runtime. That is exactly the symbolic id/url reasoning LLMs are worst at — and every id in the prompt is another thing that can be hallucinated or leak between tenants.
- Resolve the tenant first — from your sources of truth, into a generated registry of provenance-stamped bindings, optionally verified against the real systems.
- Mint a scoped capability —
for_tenant(t)captures tenantt's binding and returns a handle whose methods take only content (get("welcome")), with no tenant/namespace selector. - Let the model operate on content only — routing is already decided, in code, before the model runs.
After scoping, the model cannot redirect a correctly-implemented scoped capability to another tenant — there is no tenant, namespace, repo, or path argument on the handle to manipulate, and keys are sanitized so a value argument can't smuggle a traversal.
This is a guarantee about the capability interface, not a whole-process sandbox. A provider you write could still expose an unsafe method, share a misconfigured client, or ignore its scope; the process can reach the filesystem or credentials if those are exposed separately. What this package makes unrepresentable is tenant selection through the tool surface the agent sees. That's the boundary — a narrow, real, and load-bearing one.
pip install agent-tenancy # once published
# or from source:
git clone https://github.com/kkrlstrm/agent-tenancy
cd agent-tenancy && pip install -e .Pure standard library — zero runtime dependencies. Python 3.9+.
1. Generate a registry from a directory of tenant folders (each with a tenant.md):
tenancy generate examples/tenants -o tenants.generated.json --verifywrote tenants.generated.json — 2 tenant(s)
acme: ✓ docs_store, ✓ api_token_env
globex: ✓ docs_store, ✓ api_token_env
2. Reach a tenant's resources through a scoped capability (examples/demo.py):
from agent_tenancy import tenant, load_registry
from agent_tenancy.providers import LocalStore
reg = load_registry("tenants.generated.json")
store = LocalStore(root="./data") # one provider, configured once
acme = store.for_tenant(tenant("acme", reg)) # a handle scoped to Acme
acme.put("welcome", "hello from acme")
acme.get("welcome") # -> 'hello from acme'
globex = store.for_tenant(tenant("globex", reg))
globex.get("welcome") # -> None (can't reach Acme's data)
globex.keys() # -> [] (can't even see Acme's keys)The caller never names a namespace. There is no argument on get/put/keys through
which globex could read acme — and the test suite asserts that by inspecting the
method signatures, not just by exercising a runtime check.
Three layers, each swappable:
| Layer | What it is | Ships |
|---|---|---|
Registry (Tenant, Binding) |
Immutable tenant definition; each binding carries value / resolver / source / status / verified_at |
core |
Resolvers (Resolver) |
The generator's plug-points — one per source of truth. Resolve a binding + optionally verify it live | ABC + 2 reference resolvers |
Capabilities (Capability) |
Turn a binding into a tenant-scoped handle; the router that keeps the model off the wiring | ABC + 2 reference providers |
The two reference resolvers (MarkdownFrontmatterResolver, EnvVarResolver) and two
reference providers (LocalStore, EnvKV) exist to make the pattern runnable end-to-end
on synthetic tenants. In your own system you write the resolvers that hit your sources
and the providers that wrap your services, and register them the same way — see
Bring your own.
The registry is a generated artifact with an audit trail, not a hand-kept config. A
registry that stores only values can't tell you why a tenant is misconfigured; every
binding here is a Binding, not a bare string:
b = tenant("acme", reg).binding("docs_store")
b.value # 'acme'
b.resolver # 'markdown-frontmatter' ← which resolver produced it
b.source # 'examples/tenants/acme/tenant.md'
b.status # 'verified' ← resolved | verified | missing | unverifiable
b.verified_at # '2026-07-17'Status is a graded state, not a boolean — --verify upgrades resolved → verified by
checking each binding live (the file still exists, the env var is actually set), and
demotes to missing when it's gone. tenancy verify reports every non-usable binding
across the registry. The generator discovers, resolves, verifies, and preserves
provenance — a tenant wiring pipeline, not an injection helper.
A binding's value is an identifier or an env-var name — never the secret itself.
EnvKV reads the actual value from the environment at call time, so the generated
tenants.generated.json is safe to commit. The registry says where the secret is, not
what it is.
The public package is the mechanism; your wiring is config + plugins. A private repo uses it like this:
from agent_tenancy.generate import build_registry, TenantSource
from agent_tenancy import Resolver, Capability
class GitLabRepoResolver(Resolver): # your source of truth
name = "gitlab"
def resolve(self, src): ... # -> {"repo": Binding(...)}
def verify(self, binding): ... # -> hit glab, mark verified/missing
class GitLabFiles(Capability): # your service, tenant-scoped
binding_name = "repo"
def _scope(self, slug, project_id): ...# -> a handle that reads only that repo
reg = build_registry(my_sources, [GitLabRepoResolver(), ...], verify=True)Nothing about your tenants, services, or credentials lives in this package — only the schema, the generator, and the scoping discipline.
An agent should generate and interpret content. It should not be the thing that decides which tenant it represents, which credential to use, which repository to access, which namespace to query, which policy boundary applies, or where an action executes. Those are control-plane decisions.
agent-tenancy takes one of them — tenant and resource binding — out of the model's hands
and makes it deterministic, inspectable, and testable. It's a reference architecture for
putting a deterministic tenant boundary around a probabilistic system, and it composes
with whatever owns the other control-plane responsibilities.
The design leans on four established ideas rather than inventing new ones:
- Capability-based security — a scoped handle is an unforgeable capability: holding it grants access to exactly one tenant's resources, and there's no ambient way to name another. Authority travels with the handle, not with an argument.
- Deterministic control planes — keep the probabilistic component off the routing path; let it decide content, let code decide addressing.
- Immutable definitions — the tenant record is frozen and its bindings are read-only, so the definition can't drift under a running system; per-run state lives in the ephemeral scoped handle.
- Configuration provenance — every binding records its source and verification status, so a misconfiguration is explainable instead of mysterious.
- It's not an agent framework or orchestrator. It's the tenant-binding layer you put under one.
- It's not a generic multi-tenancy platform. No auth, authorization policy, row-level security, credential lifecycle, workload isolation, provisioning, or billing — it does one thing: bind the tenant and scope the capability.
- It doesn't manage secrets. It records where a secret lives (an env-var name); your secret manager owns the value.
- Reference resolvers/providers are illustrative. They demonstrate the pattern on synthetic tenants; production use means writing your own.
Zero dependencies, fully offline:
pip install -e ".[dev]"
pytest -qGNU AGPL-3.0-or-later — see LICENSE.
Part of a portfolio of governed, AI-native GTM systems — reference implementations and reusable patterns extracted from a private production stack. In that system this is the tenant-binding + capability-scoping layer the whole stack sits on.
Full portfolio map → github.com/kkrlstrm
Works with:
- knowledge-graph-governance — governs writes within a tenant's scope
- model-eval-gate — governs model egress within the runtime