Delegate tenant lifecycle to the tenant authority
Some checks are pending
CI Smoke / container-smoke (push) Waiting to run
CI Smoke / host-smoke (push) Successful in 0s

TEN-WP-0005 landed the authoritative metadata update and reversible
retirement contract, so USER-WP-0021-T01's deferred tenant operations are
now implementable without user-engine inventing lifecycle semantics.

TenantManagementPort gains read, update, retire, and reactivate. The HTTP
adapter echoes the record version as an If-Match ETag (never `*`), sends an
Idempotency-Key plus actor/reason/correlation_id, and surfaces
Idempotent-Replay. Authority failures map to redacted domain errors carrying
only the contract's stable error_code; its detail text never crosses the
boundary.

Platform operators get the matching API routes and a CSRF-protected browser
screen that reads the record before mutating it and hides the metadata form
for a retired tenant. Portal OpenAPI moves to 0.3.0 with TenantRecord,
UpdateTenant, and TenantLifecycleChange.

Full suite: 145 tests, 3 external-provider skips.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-16 01:28:02 +02:00
parent db8769cc8c
commit 667ea694c2
8 changed files with 952 additions and 11 deletions

View file

@ -4,11 +4,11 @@ type: workplan
title: "Expand user-engine portal beyond the proven Binky MVP"
domain: communication
repo: user-engine
status: blocked
status: active
owner: codex
topic_slug: netkingdom
created: "2026-07-30"
updated: "2026-08-10"
updated: "2026-08-16"
depends_on:
- USER-WP-0020
- TEN-WP-0005
@ -24,7 +24,7 @@ holding the proven production MVP open. Activate according to tenant demand.
```task
id: USER-WP-0021-T01
status: progress
status: done
priority: high
state_hub_task_id: "342299b8-d9a3-408d-bf0d-914496714d5f"
```
@ -222,6 +222,36 @@ Authorization headers differ. The full suite passes 132 tests with three
external-provider skips. Production rollout still waits on governed delivery
of both scoped credentials.
2026-08-16 tenant lifecycle completion: TEN-WP-0005 is finished, so the last
deferred part of T01 is now implementable against a real authority.
`TenantManagementPort` gained `tenant`, `update_tenant`, `retire_tenant`, and
`reactivate_tenant`, and `HTTPTenantManagementAdapter` implements them against
the `tenant-lifecycle-api.md` contract: version ETag as `If-Match` (never `*`),
`Idempotency-Key`, actor/reason/correlation_id, and `Idempotent-Replay`
surfaced as a `replayed` flag. Authority failures map to redacted domain errors
carrying only the contract's stable `error_code`; the authority's `detail` text
never crosses the boundary.
Platform operators get `GET`/`PATCH /api/v1/platform/tenants/{tenant}` plus
`/retire` and `/reactivate`, and a CSRF-protected browser lifecycle screen that
reads the record first and echoes its version. A retired tenant renders no
metadata form, matching the authority's own transition rules. user-engine keeps
no tenant table and invents no retirement semantics: every read and write goes
to tenant-engine. Portal OpenAPI is now 0.3.0 with `TenantRecord`,
`UpdateTenant`, and `TenantLifecycleChange` schemas, and the immutable
identifier is documented as rejected rather than discovered from a 400.
Conformance proves ordinary-user denial, stale-version 409, replayed mutation
without a second version bump, update-while-retired and double-retirement 409,
reactivation, unknown-tenant 404, forged-CSRF rejection with unchanged
lifecycle, and adapter-level redaction of authority detail text. The full suite
passes 145 tests with three external-provider skips.
This closes the engineering scope of T01. Production activation of the outbox
transport remains gated on governed delivery of the OpenBao event/mail tokens
and transactional SMTP credentials — an operational gate outside this repo, not
outstanding user-engine code.
2026-08-14 live retry finding: the PostgreSQL adapter excluded every row with
`failed_at` set, while the in-memory contract correctly retains failures until
`dead_lettered_at`. A first receiver error therefore required manual replay