tenant-engine/workplans/TEN-WP-0002-domain-model-and-scaffold.md
tegwick 9370348d54 Bootstrap repo: State Hub registration, agent docs, TEN-WP-0001/0002
statehub register + repo-seed template scaffold (CLAUDE.md, .claude/rules/,
registry/). INTENT.md and SCOPE.md rewritten from the generated stub to
match net-kingdom's ratified tenant-engine-boundary-contract_v0.1.md
(Purpose, Responsibility Boundary, Non-Goals). topic_slug corrected from
the auto-assigned custodian default to netkingdom, matching key-cape and
user-engine.

TEN-WP-0001 (bootstrap) complete: files reviewed/refined, stack decided
(Python 3.12 + FastAPI, matching qonto-assistant's convention), first real
workplan seeded.

TEN-WP-0002 drafted: service skeleton, domain model (tenant/grouping/
capability-role/plan-grant), storage layer, and the three boundary-contract
API surfaces (cache-read for key-cape, live-lookup for flex-auth with an
explicit fail-closed requirement, write API behind a WriteAuthorizer seam
since real flex-auth integration is a declared non-goal for this pass).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 21:56:07 +02:00

179 lines
6.9 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: TEN-WP-0002
type: workplan
title: "Service skeleton, domain model, and the three boundary-contract APIs"
domain: infotech
repo: tenant-engine
status: ready
owner: codex
topic_slug: netkingdom
created: "2026-07-23"
updated: "2026-07-23"
---
# Service skeleton, domain model, and the three boundary-contract APIs
First real implementation workplan. Builds the tenant-engine skeleton
against `net-kingdom/canon/standards/tenant-engine-boundary-contract_v0.1.md`:
tenant/grouping/capability-role/plan-grant domain model, an audited grant
trail, and the three API surfaces the contract defines — cache-read
(`key-cape`), live-lookup (`flex-auth`), and write (grant/revoke/plan
mutation). Guardrail/quota policy stays a reserved, unimplemented namespace
per ADR-0014 — not in scope here.
**Depends on:** `net-kingdom` canon — `iam-profile_v0.3.md`,
`tenant-engine-boundary-contract_v0.1.md`, `ADR-0013`, `ADR-0014` (all
ratified). **Non-goals:** payment processing, pricing-model definitions
(`adaptive-pricing`'s job), real `flex-auth` integration (the write/live
APIs get a policy-hook interface, not a working `flex-auth` client — that's
a follow-up once `flex-auth` has a reachable endpoint), guardrail/quota
enforcement.
## Task: Service skeleton
```task
id: TEN-WP-0002-T01
status: todo
priority: high
```
Python 3.12 + FastAPI, matching `qonto-assistant`'s layout convention:
`pyproject.toml`, `Makefile` (`install-dev`, `test`, `lint`, `run`), package
layout separating `domain/` (pure), `store/` (persistence), `api/` (FastAPI
routers), from day one — not refactored in later.
Done when: `make test` runs an empty/smoke suite; `make run` starts a bare
FastAPI app with a `/health` endpoint.
## Task: Domain model — tenant, grouping, capability role, plan grant
```task
id: TEN-WP-0002-T02
status: todo
priority: high
```
Pure domain types, no framework dependency:
- `Tenant`: id, identifier (`tenant:<grouping>:<name>` or reserved
`tenant:platform`/`tenant:coulomb`), grouping (ADR-0013 enum, nullable for
the two reserved identifiers).
- `CapabilityRole`: enum `PLTF`/`IAM`/`VEN`/`CUS`.
- `RoleGrant`: tenant_id, role, grant_reason (`plan_assignment` /
`manual_grant` / `platform_default`), plan_id (nullable), granted_by,
granted_at, revoked_at (nullable), correlation_id — the audited record
shape from the boundary contract's Tenant Role & Plan Grant Contract.
- `PlanAssignment`: tenant_id, plan_id (references an `adaptive-pricing`
plan id — stored as an opaque string, never resolved or duplicated
locally), assigned_at.
Validation rules encoded as domain invariants, not just API-layer checks:
- grouping must be one of ADR-0013's twelve values, or the tenant identifier
must be exactly `tenant:platform`/`tenant:coulomb` (grouping-less);
- a `trial`-grouped tenant may hold any role with `grant_reason:
platform_default` and no `plan_id` (ADR-0014) — this must be
representable, not blocked by a plan-required constraint;
- non-`trial` roles other than `platform_default` require a `plan_id` when
`grant_reason: plan_assignment`;
- revoking a grant sets `revoked_at`, never deletes the record (audit trail).
Done when: unit tests cover valid/invalid grouping values, the trial/
no-plan-required exception, and grant/revoke as append-only operations.
## Task: Storage layer
```task
id: TEN-WP-0002-T03
status: todo
priority: high
```
In-memory store behind a `TenantStore` protocol/interface (mirrors
`qonto_client.QontoClientProtocol`'s pattern in `qonto-assistant`), so a
real backend can be swapped in later without touching `domain/` or `api/`.
Every mutation emits a domain event (tenant created, role granted, role
revoked, plan assigned) per the boundary contract's Audit Correlation
Contract — the event bus itself can be a simple in-process list for now.
Done when: unit tests cover create/read/grant/revoke/assign-plan through the
store interface, plus event emission for each mutation.
## Task: Cache-read API (for key-cape)
```task
id: TEN-WP-0002-T04
status: todo
priority: high
```
`GET /tenants/{tenant_id}/roles` — returns current, non-revoked capability
roles for a tenant. This is the endpoint `key-cape` calls at token-issuance
time to source the cached `tenant_roles` claim (IAM Profile v0.3). No
authorization gate of its own beyond service-to-service auth (out of scope
here — see Non-Goals); this endpoint's whole purpose is to be cheap and
fast, per the boundary contract's performance model.
Done when: integration test hits the endpoint against the in-memory store
and returns the expected role set for a seeded tenant.
## Task: Live-lookup API (for flex-auth) — fail closed
```task
id: TEN-WP-0002-T05
status: todo
priority: high
```
`GET /tenants/{tenant_id}/roles/live` — same data as the cache-read
endpoint, but explicitly documented and tested as the path `flex-auth` must
call before authorizing `aal2`-class actions. The distinction from T04 is
operational intent (freshness guarantee, called synchronously on a
privileged-decision path), not payload shape.
Per the boundary contract's performance model: **fail closed, never open**.
If the store is unavailable, this endpoint must return an error response
that a policy caller would treat as "deny", not a default-allow or an empty
role list indistinguishable from "no roles granted".
Done when: a test simulates store unavailability and asserts the endpoint
signals failure distinctly from "zero roles", not silently as 200 + `[]`.
## Task: Write API — grant, revoke, assign-plan
```task
id: TEN-WP-0002-T06
status: todo
priority: medium
```
`POST /tenants` (create), `POST /tenants/{id}/roles/grant`,
`POST /tenants/{id}/roles/revoke`, `POST /tenants/{id}/plan` — all mutating
endpoints from the boundary contract's Write API. Per the contract,
`tenant-engine` does not self-authorize these writes; `flex-auth` is meant
to gate them. Since a reachable `flex-auth` integration is explicitly a
non-goal here, implement a `WriteAuthorizer` protocol/interface point (one
class, default-deny stub) so every mutation already flows through a single
seam — swapping in a real `flex-auth` client later touches one file, not
every endpoint.
Done when: unit tests confirm every write endpoint calls the
`WriteAuthorizer` seam and is denied by the default-deny stub; a
test-only authorizer override proves the seam actually gates the mutation
when swapped.
## Task: Closure review
```task
id: TEN-WP-0002-T07
status: todo
priority: low
```
Confirm T01T06 done; run `make test`/`make lint`; verify the three API
surfaces match the boundary contract's Ownership Model and Source-of-Truth
Matrix with no drift. Note follow-ups: real `flex-auth` `WriteAuthorizer`
integration, `key-cape` wiring to actually call the cache-read endpoint at
issuance, guardrail/quota policy design (ADR-0014's reserved item), and
Binky Hedgehog GmbH as the first real tenant record once `key-cape`'s
`KEY-WP-0004` reaches that point. Run `statehub fix-consistency`.