Agent Tenant
Multi-tenant isolation as a first-class primitive. Replaces the per-operator wiring of actor.tenantId === registeredTenant that tends to leak across handlers, with one centralized scope:
- **Registry** — register(tenantId, config) declares a tenant boundary at boot. The row's metadata is sealed at rest via b.cryptoField when a vault is configured (the default in a booted app), so tenant metadata doesn't leak in DB dumps. - **Cross-tenant gate** — check(actor, agentTenantId) refuses calls where actor.tenantId !== agentTenantId unless the actor holds the framework.cross-tenant-admin scope. - **Per-tenant derived keys** — derivedKey(tenantId, purpose) composes b.crypto.namespaceHash to derive a stable per- tenant key from the framework's primary seal key + tenant context. Cross-tenant decrypt refused at the vault boundary. - **Per-tenant audit** — auditFor(tenantId) returns an audit wrapper that auto-tags metadata with the tenant id so each tenant's audit trail is independently filterable. - **Archive-default destroy** — unregister(tenantId) archives the tenant + its derived key (retention-safe default). Destruction requires explicit { destroy: true, stepUpToken, dualControlApprover, reason } — irreversible crypto-erasure for GDPR Art. 17 / right-to-be-forgotten cases.
var tenant = b.agent.tenant.create({});
await tenant.register("acme-clinic", {
posture: ["hipaa"],
archivePolicy: "hipaa-6yr",
});
tenant.check({ id: "u1", tenantId: "acme-clinic" }, "acme-clinic"); // OK
tenant.check({ id: "u2", tenantId: "globex" }, "acme-clinic"); // throws
var sealKey = tenant.derivedKey("acme-clinic", "seal");
var auditA = tenant.auditFor("acme-clinic");
b.agent.tenant.create(opts) #
{
backend: { get, set, delete, list }, // optional; in-memory default
audit: b.audit namespace, // optional
permissions: b.permissions instance, // optional
}
Create the tenant-scope facade. Returns an instance with register / unregister / lookup / list / check / derivedKey / auditFor.
var tenant = b.agent.tenant.create({});
await tenant.register("acme-clinic", { posture: ["hipaa"] });
var key = tenant.derivedKey("acme-clinic", "seal");
b.agent.tenant.derivedKey(tenantId, purpose) #
Derive a deterministic, domain-separated 32-byte key for a tenant and a named purpose, returned as a 64-char hex string. The key is a SHAKE256 KDF over the vault root (the master keypair PEM hashed), the tenantId, and the purpose, with NUL separators so distinct (tenantId, purpose) pairs cannot collide. The same inputs always produce the same key, so a value sealed under derivedKey(t, "archive-wrap") is recoverable later from the same tenant + purpose with no key escrow. Rotating the vault (b.vaultRotate.rotate) changes the root and therefore every derived key, so every cell sealed under the old root must be re-sealed under the new one. That migration runs through the module's reseal hook (eager-registered via AAD_ROTATION), not silently on the next read — a value sealed under the old root does not decrypt under the new root until the rotation pipeline walks it.
Throws if the vault has not been initialized (keys cannot be derived before bootstrap) or if purpose is empty. This is the same derivation the per-tenant sealField / archive recipient: "tenant" paths use internally; call it directly when you need the raw key for your own AEAD.
var key = b.agent.tenant.derivedKey("acme-corp", "archive-wrap");
// → "9f3c…" (64 hex chars; deterministic per tenant + purpose)
Last updated 2026-08-08T16:39:49.652Z by seeder.