Cross-tenant credentials
Read across every tenant in your org with a single credential — for admin agents, analytics, and rollups.
A cross-tenant credential lets one agent read memories, documents, and stats across multiple tenants in an org without rotating keys when tenants are added or removed. Writes still pin to the credential's home tenant — the widening is read-only by construction.
The headline use case: a single Admin agent that summarises activity across every fleet, surfaces conflicts that span teams, or builds an org-wide knowledge view. Mint the credential once; it stays current as your org grows.
What the kind buys you
| Capability | Tenant-scoped (user_api_key, agent_key) | Cross-tenant (cross_tenant) |
|---|---|---|
| Read from home tenant | ✅ | ✅ |
| Read from sibling tenants in the org | ❌ (403) | ✅ |
| Write to home tenant | ✅ | ✅ |
| Write to sibling tenants | n/a | ❌ (403 — writes pin to home) |
| Auto-include tenants added after mint | n/a | ✅ (with read_all_org_tenants=true) |
| Wire prefix | mc_ | mc_ (kind is on the row, not the prefix) |
Two ways to scope the read set
Pick one at mint time:
source_tenant_ids — a frozen, explicit list
Pin the readable set to specific sibling tenants. The list is recorded on the credential row; adding a new tenant to the org does not widen this credential. Rotate the credential to include new tenants.
Use when the cross-tenant relationship is bounded by intent (e.g. "the EU rollup agent reads from eu-sales, eu-support, eu-product and never anywhere else"). A future tenant added to the org should not be readable by this credential without an explicit decision.
read_all_org_tenants=true — live resolution
Re-resolves the readable set on every authenticated request by querying the current enterprise.tenants membership for the credential's org. New tenants are auto-included on the next request — no rotation needed. Removed (soft-deleted) tenants disappear from the readable set on the same cadence.
Use when "every tenant in the org" is the policy — the org Admin agent, the compliance reporter, the cross-team weekly summary.
The two modes are mutually exclusive — a single credential carries either an explicit source_tenant_ids list or read_all_org_tenants=true, never both. The admin-api enforces this at mint time.
Mint via the dashboard
Settings → Organization → API Credentials → Add credential → Cross-tenant. The wizard presents a binary "Read scope" toggle:
- Home tenant only — produces a regular
user_api_key/agent_keyrow. - Read across all org tenants — produces a
cross_tenantrow withread_all_org_tenants=true. The static-list variant (source_tenant_ids) is reserved for the API; the dashboard intentionally surfaces only the live-resolution mode because the audit-evidence trail concluded most operators mean "all, current and future" when they ask for cross-tenant.
The raw_key is returned once. Treat it like a password.
Mint via the API
curl -X POST "https://memclaw.net/api/v1/admin/orgs/$ORG_ID/api-credentials" \
-H "Authorization: Bearer $ADMIN_JWT" \
-H "Content-Type: application/json" \
-d '{
"kind": "cross_tenant",
"home_tenant_id": "admin-fleet",
"read_all_org_tenants": true,
"label": "org-admin-summary-agent",
"capabilities": ["read", "write"]
}'For the static-list variant, swap read_all_org_tenants: true for source_tenant_ids: ["sibling-a", "sibling-b"].
What you'll see on the wire
The gateway plumbs the resolved scope on every request:
X-Tenant-ID: admin-fleet
X-Readable-Tenant-IDs: admin-fleet,sibling-a,sibling-b,sibling-c
X-Capabilities: read,write
X-Auth-Mode: cross-tenant/whoami surfaces the same shape so you can verify your credential without probing each tenant:
{
"tenant_id": "admin-fleet",
"readable_tenant_ids": ["admin-fleet", "sibling-a", "sibling-b", "sibling-c"],
"capabilities": ["read", "write"],
"auth_mode": "cross-tenant",
"via_gateway": true
}Which surfaces honor the widening
Cross-tenant credentials widen WHERE tenant_id = $home to WHERE tenant_id = ANY($readable) on these read paths:
- MCP tools:
memclaw_recall,memclaw_list(scope=fleet|all),memclaw_stats(scope=fleet|all),memclaw_docopssearch/read/query/list_collections. - REST search/recall:
POST /search,POST /recall,POST /documents/search. - REST aggregate lists:
GET /memories(when no explicittenant_idquery param),GET /memories/stats,GET /documents/collections.
For the storage-routed reads (GET /documents/{id}, POST /documents/query, GET /documents, GET /memclaw/keystones), cross-tenant credentials access one tenant at a time — pin tenant_id to any tenant in your readable set and the gate allows it.
Writes always pin to home_tenant:
- ❌
memclaw_writewithtenant_idnot equal to home →FORBIDDEN. - ❌
POST /documents(write) to a sibling tenant → 403. - ❌
POST /memoriesto a sibling → 403.
The principle: the credential carries {read, write} capabilities, but enforce_tenant on the write target rejects anything that isn't home. Reads use enforce_readable_tenant, which accepts any tenant in the readable set.
Audit trail
Every widened read emits a cross_tenant_read audit event per source tenant the query touched. Each event is logged TO the source tenant (so per-tenant audit-log queries surface "who read FROM my tenant") with the home tenant_id and agent_id in detail for forensic traceability:
{
"action": "cross_tenant_read",
"tenant_id": "sibling-a",
"agent_id": "admin-rollup-bot",
"resource_type": "memclaw_recall",
"detail": {
"home_tenant_id": "admin-fleet",
"home_agent_id": "admin-rollup-bot",
"result_count_from_this_tenant": 7,
"query_summary": "what did we ship last week"
}
}Single-tenant credentials emit zero cross_tenant_read events — the audit is gated on the readable set being wider than home.
Operational notes
- Live resolution on every request. The
read_all_org_tenants=truepath querieslist_tenants_by_orgon every authenticated call. For high-traffic admin agents this is the next caching target — a short in-process LRU keyed onorg_idwould amortise the round-trip without making new tenants invisible for longer than feels noticeable. - Fail-open on storage degradation. If the live-resolution lookup fails, the credential degrades to home-only rather than 5xx-ing the whole request. Logged for observability; the credential keeps working with reduced scope until storage recovers.
- Revocation is immediate. Setting
revoked_atshort-circuits the resolver before the org-tenants lookup — revoked credentials can't even probe org membership. - The on-the-wire
mc_prefix is shared. Tenant-scoped and cross-tenant credentials look identical at the protocol layer. The dashboard's "Kind" column tells you which is which.
When NOT to use cross-tenant
- Per-fleet bots that should only see their own tenant's memories — use a regular
agent_key. The trust-and-fleet system handles intra-tenant scoping; cross-tenant widening is for org-wide views. - Customer-facing integrations where a single org's tenants represent different customer accounts — use one tenant-scoped credential per customer, not one cross-tenant credential that erases the boundary.
- Write-heavy agents — cross-tenant only widens reads. If your agent's primary job is writing across many tenants, run one tenant-scoped agent per tenant instead and use a coordinator pattern at the application layer.
Related
- Trust levels — the per-agent permission scheme that gates reads/writes within a tenant.
- Governance — org/tenant/agent hierarchy and how cross-tenant credentials fit into it.
- Per-agent keys — agent-scoped credentials, the more common scope for production fleets.