net-kingdom/docs/adr/ADR-0014-tenant-capability-roles-and-tenant-engine-ownership.md
tegwick bee22db620
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
docs(canon): reconcile workload and tenant grouping semantics
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02929-244b-7391-b933-c04010e8eedb
2026-08-22 14:53:31 +02:00

183 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
id: NK-ADR-0014
type: architecture-decision-record
title: "Tenant Capability Roles, Carrying Mechanism, and Tenant-Engine Ownership"
status: accepted
owner: net-kingdom
revision: "1"
decided: "2026-07-23"
last_reviewed: "2026-08-22"
review_interval: 12m
---
# ADR-0014 - Tenant Capability Roles, Carrying Mechanism, and Tenant-Engine Ownership
**Status:** Accepted
**Date:** 2026-07-23
**Deciders:** Bernd Worsch, Codex
## Context
ADR-0013 introduced the tenant onboarding grouping taxonomy
(`trial`/`friendly`/`single`/.../`agentic`), deliberately orthogonal to a
separate, unratified **capability-role** model sketched in
`docs/princedom-isolation-exploration.md`: `PLTF` (operates the platform),
`IAM` (organizes its own users/auth/secrets), `VEN` (provides apps/services
to others), `CUS` (consumes apps/services from `PLTF` or `VEN` tenants) —
non-exclusive, a tenant may hold several at once.
That exploration left open where capability roles actually live (a per-token
claim vs. a registry), who owns them, how they're granted or revoked, and
how this interacts with the IAM Profile's existing `roles` claim — which is
a *per-subject* claim ("coarse identity roles" for the human/service/agent
holding the token), a different concept from a *per-tenant* capability fact.
Conflating the two would be a category error: `roles: ["VEN"]` on a token
would ambiguously mean "this subject has vendor-role" vs. "this subject's
tenant is a vendor."
No existing service owns tenant-as-an-entity facts (existence, grouping,
capability roles, plan/subscription state) as a queryable resource.
`user-engine`'s own boundary contract
(`canon/standards/user-engine-boundary-contract_v0.1.md`) explicitly scopes
`user-engine` to *consuming* tenant identifiers and *storing tenant-scoped
records*, not owning tenant identity or capability facts.
Bernd's direction (2026-07-23):
- Implement the previously-discussed hybrid carrying approach: cache a
tenant's capability roles on the token for ordinary decisions, but
require a live check for critical/high-stakes actions.
- Role grants are usually tied to a payment plan — most concretely, `IAM`
means the tenant has its own dedicated key-cape/Keycloak instance for
isolation, scale, and performance, which is itself a paid capability.
- `trial`-grouped tenants may hold **any** capability role without
restriction, specifically so the platform can showcase, test, and explore
every role. Safety for trial tenants comes from resource guardrails
(spend limits defaulting to zero budget, entity/action count limits), not
from role gating — guardrail design itself is future work, not this ADR.
- A new service, `tenant-engine`, will be built (Bernd) as the owner of this
domain, organized beside `user-engine` rather than inside it — smaller,
single-purpose services are easier to reason about and drift less, matching
the fleet's existing convention (`activity-core`, `audit-core`, `user-engine`,
and others).
## Decision
1. **Capability-role vocabulary ratified as core NetKingdom vocabulary**:
`PLTF`, `IAM`, `VEN`, `CUS`, non-exclusive. `IAM` specifically means: the
tenant operates its own dedicated IAM implementation instance (lightweight
key-cape or expanded Keycloak) rather than sharing the platform's, for
isolation/scale/performance — not "any tenant that happens to have users."
2. **`tenant-engine` is the canonical owner** of tenant-domain facts: tenant
existence, grouping (ADR-0013), capability roles (this ADR), plan/
subscription assignment, and — reserved for future design, not built now —
guardrail/quota policy. It is a new, separate service, not a module inside
`user-engine`. Its ownership boundary is defined in the companion contract,
`canon/standards/tenant-engine-boundary-contract_v0.1.md`.
3. **Carrying mechanism: hybrid cache + live re-validation.** `tenant-engine`
is the single source of truth. `key-cape` stamps a cached, optional
`tenant_roles` claim onto issued tokens at issuance time, sourced from
`tenant-engine` (added to the IAM Profile as a new optional claim —
`canon/standards/iam-profile_v0.3.md`). Consumers may trust the cached
claim for ordinary decisions. `flex-auth` **MUST** re-validate live
against `tenant-engine` — never trust the cached claim alone — before
authorizing privileged or high-stakes actions, using the same threshold
class the profile already defines for `assurance.level >= aal2`
(privileged, destructive, platform-root, secret, credential-vending
flows). This bounds staleness risk for ordinary actions to a token's
short lifetime (530 minutes for service/agent tokens, per the profile's
Token Lifecycle table) while guaranteeing freshness exactly where it
matters most.
4. **Role governance is plan-linked.** Granting a role is normally a
consequence of a tenant's payment-plan state in `tenant-engine`, not a
separate manual workflow — starting with `IAM`. `tenant-engine` records
which plan grants which role(s); `adaptive-pricing` remains the source of
plan/pricing-model *definitions*, `tenant-engine` owns the tenant's
*current* plan/subscription assignment, referenced by id, never
duplicated locally. Whether `VEN` needs an approval gate beyond payment
(reselling access carries legal/compliance exposure a payment alone
doesn't cover) is **not resolved by this ADR** — left to `tenant-engine`'s
own workplan.
5. **Trial tenants may hold any capability role, unrestricted.** The `trial`
grouping's purpose (showcase, test, explore) requires demonstrating every
role. Safety is enforced through resource guardrails instead: trial
tenants default to a **spend budget of zero**, with entity and action
count limits to follow. Guardrail policy design (exact limits,
enforcement point, override process) is real, near-term future work,
reserved as `tenant-engine`'s to own once designed — not specified by
this ADR.
6. **Grouping and capability role are independent axes** recorded on the
same tenant record in `tenant-engine`. Neither constrains the other
except where a future guardrail policy explicitly says so.
## Consequences
- `canon/standards/iam-profile_v0.3.md` adds the optional `tenant_roles`
claim and folds in ADR-0013's tenant-identifier vocabulary update (both
non-breaking per ADR-0011's own minor-version rule — no existing
implementation is invalidated by either change). Supersedes
`iam-profile_v0.2.md`.
- `tenant-engine` becomes a new repository with its own workplans (Bernd).
`canon/standards/tenant-engine-boundary-contract_v0.1.md` defines its
ownership boundary now, before code exists — the same sequencing
`user-engine`'s contract followed.
- `flex-auth` policy packages gating high-stakes actions must add a
`tenant-engine` live-lookup step; they cannot trust `tenant_roles` alone
for those decisions.
- `key-cape` needs a `tenant-engine` integration at token-issuance time to
source the cached claim — tracked in `key-cape`'s own workplans, not here.
- `docs/platform-identity-security-architecture.md`'s Tenant Model section
is updated to reflect the grouping + role split and `tenant-engine`'s role
(companion change alongside this ADR).
- Guardrail/quota policy is named as required near-term work and given an
owner (`tenant-engine`), but is explicitly not designed by this ADR.
## Alternatives Considered
### Token-claim-only, no live re-validation
Rejected: staleness would be unbounded within a token's lifetime for
genuinely high-stakes actions. A stale `VEN` grant surviving a plan
downgrade or cancellation is not an acceptable risk for money-movement or
credential-vending flows — exactly the class the profile already treats as
requiring the strongest assurance.
### Registry-only, no cached claim
Rejected: every ordinary request would pay a `tenant-engine` round-trip even
for non-critical checks, adding latency and a hard runtime dependency for
every consumer, not just the ones handling privileged actions.
### Restrict role eligibility by tenant grouping (e.g., `trial` cannot hold `VEN`)
Rejected per Bernd's direction: `trial` tenants exist specifically to
showcase every role. Resource guardrails are the intended safety mechanism
instead, keeping the two axes (grouping, role) independent.
### Put tenant-role/plan storage inside `user-engine`
Rejected. `user-engine`'s own boundary contract scopes it to consuming
tenant identifiers and storing tenant-*scoped* user records, not owning
tenant-as-an-entity facts. A dedicated service avoids coupling a
security-critical, high-frequency lookup (used by `flex-auth` on every
privileged decision, and by `key-cape` on every token issuance) to
`user-engine`'s much larger surface (registration flows, factor models,
family dataspace onboarding) that has nothing to do with tenant capability
state.
## Follow-Up
- `tenant-engine` repository creation and its own workplan (Bernd).
- `key-cape` integration: source `tenant_roles` from `tenant-engine` at
token issuance.
- `flex-auth` policy package updates: live `tenant-engine` re-validation
gate for privileged actions.
- Guardrail/quota policy design for `trial` (and eventually all) tenants:
spend limits, entity/action count limits, enforcement point, override
process.
- Resolve whether `VEN` needs an approval gate beyond payment-plan state.