Finish TEN-WP-0006-T02: implement guardrail domain model
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
0d1435c2d2
commit
33ceb882ee
6 changed files with 868 additions and 1 deletions
99
src/tenant_engine/guardrail/resolution.py
Normal file
99
src/tenant_engine/guardrail/resolution.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Mapping
|
||||
|
||||
from tenant_engine.domain import Tenant, TenantLifecycle
|
||||
from tenant_engine.guardrail.model import EffectiveLimit, LimitValue, Provenance
|
||||
from tenant_engine.guardrail.registry import DEFAULT_REGISTRY, LimitRegistry
|
||||
|
||||
# Per-key precedence, highest first: override -> plan -> grouping (or the
|
||||
# reserved profile) -> fail-closed floor. Evaluated *per key*, never per set,
|
||||
# so a plan supplying `spend.monthly` does not wipe out a grouping-derived
|
||||
# `entity.*`.
|
||||
|
||||
|
||||
def resolve_limit(
|
||||
key: str,
|
||||
*,
|
||||
tenant: Tenant,
|
||||
plan_limits: Mapping[str, LimitValue] | None = None,
|
||||
overrides: Mapping[str, LimitValue] | None = None,
|
||||
registry: LimitRegistry = DEFAULT_REGISTRY,
|
||||
) -> EffectiveLimit:
|
||||
"""Resolve one limit key for one tenant. Total and side-effect-free.
|
||||
|
||||
Raises `UnknownLimitKeyError` for an unregistered key -- neither zero nor
|
||||
unlimited, both of which would be lies.
|
||||
"""
|
||||
definition = registry.get(key)
|
||||
|
||||
override = (overrides or {}).get(key)
|
||||
if override is not None:
|
||||
return _clamp(EffectiveLimit(key, override, Provenance.OVERRIDE), tenant, definition.floor)
|
||||
|
||||
plan_limit = (plan_limits or {}).get(key)
|
||||
if plan_limit is not None:
|
||||
return _clamp(EffectiveLimit(key, plan_limit, Provenance.PLAN), tenant, definition.floor)
|
||||
|
||||
if tenant.is_reserved:
|
||||
# Reserved identifiers are ungrouped, so layer 3 cannot apply. They do
|
||||
# not fall through to the floor either: that would clamp the platform's
|
||||
# own identity to zero and take the platform down with it.
|
||||
return _clamp(
|
||||
EffectiveLimit(key, definition.reserved_default, Provenance.RESERVED),
|
||||
tenant,
|
||||
definition.floor,
|
||||
)
|
||||
|
||||
grouping_default = definition.grouping_defaults.get(tenant.grouping or "")
|
||||
if grouping_default is not None:
|
||||
return _clamp(
|
||||
EffectiveLimit(key, grouping_default, Provenance.GROUPING), tenant, definition.floor
|
||||
)
|
||||
|
||||
# Only reachable if the registry is internally inconsistent -- a grouping
|
||||
# added to GROUPINGS with no default declared. The registry validates this
|
||||
# at construction, so this is the second line of defence, not the first.
|
||||
# Provenance stays `fail_closed` so the condition is visible in a read
|
||||
# rather than mistaken for deliberate policy.
|
||||
return _clamp(
|
||||
EffectiveLimit(key, definition.floor, Provenance.FAIL_CLOSED), tenant, definition.floor
|
||||
)
|
||||
|
||||
|
||||
def resolve_limits(
|
||||
*,
|
||||
tenant: Tenant,
|
||||
plan_limits: Mapping[str, LimitValue] | None = None,
|
||||
overrides: Mapping[str, LimitValue] | None = None,
|
||||
registry: LimitRegistry = DEFAULT_REGISTRY,
|
||||
) -> dict[str, EffectiveLimit]:
|
||||
"""Resolve every registered key. Every tenant resolves to exactly one
|
||||
effective value per key -- there is no "unset means unlimited".
|
||||
"""
|
||||
return {
|
||||
key: resolve_limit(
|
||||
key,
|
||||
tenant=tenant,
|
||||
plan_limits=plan_limits,
|
||||
overrides=overrides,
|
||||
registry=registry,
|
||||
)
|
||||
for key in registry.keys
|
||||
}
|
||||
|
||||
|
||||
def _clamp(effective: EffectiveLimit, tenant: Tenant, floor: LimitValue) -> EffectiveLimit:
|
||||
"""Apply the lifecycle clamp. Applied after precedence, and may only reduce.
|
||||
|
||||
Follows the TEN-WP-0005 precedent: operations that only reduce privilege
|
||||
stay available while retired, loosening ones do not. A retired tenant's
|
||||
guardrails stay readable -- it is the values that clamp, not the endpoint.
|
||||
"""
|
||||
if tenant.lifecycle is TenantLifecycle.ACTIVE:
|
||||
return effective
|
||||
|
||||
clamped = effective.value.reduced_to_floor(floor)
|
||||
if clamped == effective.value:
|
||||
return effective
|
||||
return EffectiveLimit(effective.key, clamped, Provenance.LIFECYCLE)
|
||||
Loading…
Add table
Add a link
Reference in a new issue