Problem
Resources (UAITs, credentials, delegations, compliance profiles, etc.) have no concept of tenant ownership. The whole engine assumes a single global namespace, which is fine for self-host but means the cloud product cannot store multiple customers' data side-by-side without engine forks.
The cleanest fix is upstream: add an optional tenant_id field that defaults to a sentinel value ("default") for self-host installs. Self-hosters never see it; cloud uses it for RLS scoping and audit chain isolation per workspace.
Proposed solution
Thread tenant_id through the resource models and the signable payload:
# services/identity_service.py
def create_identity(
self,
display_name: str,
source_protocol: str,
*, # keyword-only from here
tenant_id: str = "default", # NEW
identity_token: str = "",
capabilities: Optional[List[str]] = None,
description: str = "",
issuer_name: str = "",
expiry_days: Optional[int] = None,
) -> dict:
...
uait = {
"version": UAIT_VERSION,
"agent_id": agent_id,
"tenant_id": tenant_id, # NEW field on the resource
"display_name": display_name,
...
}
Same pattern on:
CredentialService.issue_credential -> credential gets tenant_id
DelegationService.create_delegation -> delegation gets tenant_id
ComplianceService.create_profile -> profile gets tenant_id
ProvenanceService.record_* -> provenance entry gets tenant_id
ReputationService.record_interaction -> interaction gets tenant_id
Optional read-side filtering:
def list_identities(
self,
*,
tenant_id: Optional[str] = None, # NEW; None means "no filter"
source_protocol: Optional[str] = None,
include_revoked: bool = False,
limit: int = 50,
) -> List[dict]:
data = self.storage.load_identities()
results = []
for agent in data["agents"]:
if tenant_id is not None and agent.get("tenant_id", "default") != tenant_id:
continue
...
tenant_id is part of the signable payload (immutable, included in _signable_payload). Existing self-host data without the field reads as "default" for verification purposes - this needs careful handling so existing signatures still verify after the upgrade.
Backward compatibility
- Existing JSON files have no
tenant_id field. Loaders treat missing field as "default".
- Existing signatures over old payloads continue to verify because
_signable_payload should treat absent tenant_id as "default" rather than as a missing field. Document this explicitly in auth/crypto.py.
- API responses gain a
tenant_id key but self-host clients can ignore it.
Acceptance criteria
Dependencies
Soft-blocked by # because tenant_id filtering is more natural once the storage layer is pluggable (cloud's PostgresStorage will push the filter into a SQL WHERE tenant_id = $1 clause via RLS rather than filtering in Python). Can be done before but the integration with cloud is cleaner after.
Cloud context
Cloud's Postgres adapter sets app.tenant_id GUC per request and RLS policies on every table use it. This OSS PR adds the field on the resource so the cryptographic surface stays consistent across self-host and cloud.
Reference: attestix-cloud-plan/07-DATA-MODEL.md (multi-tenant RLS), 02-OPEN-CORE.md "What changes in attestix because of cloud".
Suggested commit message
feat(tenant): optional tenant_id field on resource models
Adds an optional tenant_id field (default: "default") to UAIT,
credential, delegation, compliance profile, provenance entry, and
reputation interaction models. Threaded through service create/list
methods as a keyword-only parameter.
tenant_id is part of the signable payload; legacy data without the
field is treated as tenant_id="default" for both reads and signature
verification, preserving compatibility with existing self-host
deployments.
Refs: #<issue>
Problem
Resources (UAITs, credentials, delegations, compliance profiles, etc.) have no concept of tenant ownership. The whole engine assumes a single global namespace, which is fine for self-host but means the cloud product cannot store multiple customers' data side-by-side without engine forks.
The cleanest fix is upstream: add an optional
tenant_idfield that defaults to a sentinel value ("default") for self-host installs. Self-hosters never see it; cloud uses it for RLS scoping and audit chain isolation per workspace.Proposed solution
Thread
tenant_idthrough the resource models and the signable payload:Same pattern on:
CredentialService.issue_credential-> credential getstenant_idDelegationService.create_delegation-> delegation getstenant_idComplianceService.create_profile-> profile getstenant_idProvenanceService.record_*-> provenance entry getstenant_idReputationService.record_interaction-> interaction getstenant_idOptional read-side filtering:
tenant_idis part of the signable payload (immutable, included in_signable_payload). Existing self-host data without the field reads as"default"for verification purposes - this needs careful handling so existing signatures still verify after the upgrade.Backward compatibility
tenant_idfield. Loaders treat missing field as"default"._signable_payloadshould treat absenttenant_idas"default"rather than as a missing field. Document this explicitly inauth/crypto.py.tenant_idkey but self-host clients can ignore it.Acceptance criteria
tenant_id, listing withtenant_idfilter, signature verification on a payload that explicitly carriestenant_id, signature verification on a legacy payload that lacks it.--tenant-id; defaults to"default".Dependencies
Soft-blocked by # because
tenant_idfiltering is more natural once the storage layer is pluggable (cloud's PostgresStorage will push the filter into a SQLWHERE tenant_id = $1clause via RLS rather than filtering in Python). Can be done before but the integration with cloud is cleaner after.Cloud context
Cloud's Postgres adapter sets
app.tenant_idGUC per request and RLS policies on every table use it. This OSS PR adds the field on the resource so the cryptographic surface stays consistent across self-host and cloud.Reference:
attestix-cloud-plan/07-DATA-MODEL.md(multi-tenant RLS),02-OPEN-CORE.md"What changes in attestix because of cloud".Suggested commit message