from __future__ import annotations from dataclasses import dataclass, replace from datetime import datetime from enum import Enum from typing import Literal # ADR-0013: onboarding-risk / entity-shape grouping. Orthogonal to # capability role (below) -- neither axis constrains the other except # through guardrail policy, which is a reserved, unimplemented concern. GROUPINGS = frozenset( { "trial", "friendly", "single", "small", "medium", "large", "enterprise", "consumer", "family", "community", "association", "agentic", } ) # tenant:platform and tenant:coulomb predate the grouping taxonomy and stay # reserved, ungrouped identifiers (ADR-0013's Tenant Claim rationale). RESERVED_IDENTIFIERS = frozenset({"tenant:platform", "tenant:coulomb"}) class InvalidTenantIdentifierError(ValueError): """A tenant identifier does not match `tenant::` or a reserved form.""" class InvalidGrantError(ValueError): """A role grant violates a domain invariant (ADR-0014).""" class CapabilityRole(str, Enum): """ADR-0014: non-exclusive capability roles a tenant may hold.""" PLTF = "PLTF" IAM = "IAM" VEN = "VEN" CUS = "CUS" GrantReason = Literal["plan_assignment", "manual_grant", "platform_default"] def parse_tenant_identifier(identifier: str) -> tuple[str | None, str]: """Return (grouping, name). grouping is None only for reserved identifiers.""" if identifier in RESERVED_IDENTIFIERS: return None, identifier.split(":", 1)[1] parts = identifier.split(":") if len(parts) != 3 or parts[0] != "tenant": raise InvalidTenantIdentifierError( f"Malformed tenant identifier: {identifier!r} (expected tenant::)" ) _, grouping, name = parts if grouping not in GROUPINGS: raise InvalidTenantIdentifierError(f"Unknown tenant grouping: {grouping!r}") if not name: raise InvalidTenantIdentifierError("Tenant name segment is empty") return grouping, name @dataclass(frozen=True, slots=True) class Tenant: tenant_id: str identifier: str grouping: str | None @classmethod def create(cls, *, tenant_id: str, identifier: str) -> "Tenant": grouping, _name = parse_tenant_identifier(identifier) return cls(tenant_id=tenant_id, identifier=identifier, grouping=grouping) @property def is_reserved(self) -> bool: return self.grouping is None @dataclass(frozen=True, slots=True) class RoleGrant: """Audited role grant record -- the Tenant Role & Plan Grant Contract shape. Append-only: revocation produces a new record via `revoke()`, it never deletes the original. """ grant_id: str tenant_id: str role: CapabilityRole grant_reason: GrantReason plan_id: str | None granted_by: str granted_at: datetime correlation_id: str revoked_at: datetime | None = None @property def active(self) -> bool: return self.revoked_at is None def revoke(self, *, at: datetime) -> "RoleGrant": if self.revoked_at is not None: raise InvalidGrantError(f"Grant {self.grant_id!r} is already revoked") return replace(self, revoked_at=at) @dataclass(frozen=True, slots=True) class PlanAssignment: """A tenant's current plan, referenced by id only -- never resolved or duplicated locally. Plan term definitions belong to adaptive-pricing. """ tenant_id: str plan_id: str assigned_at: datetime def create_role_grant( *, tenant: Tenant, grant_id: str, role: CapabilityRole, grant_reason: GrantReason, plan_id: str | None, granted_by: str, correlation_id: str, granted_at: datetime, ) -> RoleGrant: """Construct a RoleGrant, enforcing ADR-0014's domain invariants. - `plan_assignment` grants always require a `plan_id`. - `platform_default` grants never carry a `plan_id`, and are only valid for `trial`-grouped tenants (ADR-0014: trial may hold any role, unrestricted, for showcase purposes) or the reserved, ungrouped `tenant:platform`/`tenant:coulomb` tenants (their baseline roles were never purchased either). - `manual_grant` carries no grouping restriction and an optional `plan_id`. """ if grant_reason == "plan_assignment" and plan_id is None: raise InvalidGrantError("plan_assignment grants require a plan_id") if grant_reason == "platform_default": if plan_id is not None: raise InvalidGrantError("platform_default grants must not carry a plan_id") if tenant.grouping not in (None, "trial"): raise InvalidGrantError( "platform_default is only valid for trial-grouped or reserved tenants, " f"got grouping={tenant.grouping!r}" ) return RoleGrant( grant_id=grant_id, tenant_id=tenant.tenant_id, role=role, grant_reason=grant_reason, plan_id=plan_id, granted_by=granted_by, granted_at=granted_at, correlation_id=correlation_id, )