An AI agent can hold several Biloh connectors at once — one per tenant, plus a platform-admin connector — and they look almost identical in the tool list. The danger is not that one connector reads another's data (that is sealed by a tenant-bound token, row-level security, and write gateways, independent of the agent). The danger is intent: calling the right tool on the wrong connector.
Biloh solves this in three layers, cheapest first: make the tenant visible, make it assertable, and gate the actions you can't undo. Visibility alone — the most common instinct — is not enough.
Layer 1 — make the tenant visible
Two signals, on by default:
- Connector identity. Each connection advertises itself as
biloh · <Tenant> (<MODE>), where mode isLIVE(a real tenant),TEST(a tenant flagged as test data), orPLATFORM(the cross-tenant admin surface). Two real tenants are bothLIVE— so the name distinguishes them and the mode separates a tenant from the platform. - A per-response stamp. Every tool result carries
meta.tenant { id, name, mode }. Because it is on every call, the last result an agent received always tells it which tenant it just touched.
A subtle implementation note: the MCP handler is built once per process, so the server's identifying info is otherwise static across connections. Per-connection identity is therefore applied by rewriting the protocol initialize response — fail-safe, so any anomaly returns the original handshake untouched rather than risking the connection.
Layer 2 — make the tenant assertable (a misroute should fail)
Visibility helps a careful reader. It does nothing for a mistake already in flight. So a write can carry an optional expected_tenant: the tenant you intend to act on, by name or id.
If it doesn't match the connector you're on, the call returns expected_tenant_mismatch and the handler never runs. A misroute stops instead of silently going through. The argument rides the same central schema-augmentation as cross-tenant routing and is enforced once, at the single wrapper every tool call passes through — so it covers the whole tool surface without per-tool code.
The principle: data isolation and intent safety are different problems. One is enforced by the database; the other has to be enforced by affordances the caller opts into.
Layer 3 — gate what you can't undo
Two-phase confirm-gates protect the irreversible actions. The first call returns a preview plus a deterministic confirm_token and mutates nothing; only a second call carrying the matching token executes.
The scoping rule matters more than the mechanism: gate by operation, not by keyword. Archiving a tenant is irreversible — gated. Creating a tenant is gated too (it completes the lifecycle pair). Minting an access token is revocable — so it is deliberately left ungated. "Sounds dangerous" is not the test; "can't be undone" is.
A gate is only safe to add after an audit confirms nothing automated calls the tool — otherwise you break provisioning. In this build the audit showed zero code-level callers, so the gate had zero blast radius.
What each layer is locked by
Every layer ships with a spec test written before the code, so the guarantee can't quietly regress:
- The mismatch guard has a test asserting a wrong
expected_tenantreturns the error code and the handler does not run. - Each confirm-gate has a test asserting the no-token call previews and mutates nothing, a wrong token errors, and the matching token executes.
- The identity rewrite is fail-safe by construction and tested against a malformed handshake.
Next steps
- The connection basics: Connecting Biloh over MCP.
- How an agent finds the right tool in the first place: Tool discoverability for agents.