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

View file

@ -8,7 +8,7 @@ status: blocked
owner: codex
topic_slug: netkingdom
created: "2026-08-09"
updated: "2026-08-10"
updated: "2026-08-16"
depends_on:
- USER-WP-0021
- NK-WP-0025
@ -110,6 +110,20 @@ creating another session or requesting another email; reuse with different
inputs returns 409. Raw keys and applicant fields are absent from idempotency
evidence. The full suite passes 134 tests with three external skips.
2026-08-16 gate review: every implementation item listed for this task is
built and covered — accessible start/verify/resume/cancel entry points,
allow-listed anonymous routes, opaque intent handles, CSRF, per-peer rate
limiting, required idempotency keys, and non-enumerating responses. The
application-level registration limit is configured in the runtime manifest on
top of the Traefik-wide limit, closing the ingress defense-in-depth item.
What remains is not user-engine code. Public registration stays disabled until
the OpenBao verification and delivery tokens plus the transactional SMTP lane
are delivered through governed channels. The service already fails closed when
they are absent, and no placeholder or reused credential will be added to
unblock it. This task stays in `wait` because the remaining gate is a
credential-custody handoff, not an outstanding change here.
## T02 - Orchestrate provider identity creation
```task
@ -221,6 +235,13 @@ the identity-link uniqueness boundary, leaves the original link unchanged, and
never transfers that identity to the later registration. The full suite passes
131 tests with three environment-dependent skips.
2026-08-16 authorization evidence: flex-auth reports FLEX-WP-0009 finished and
handed back deployed-policy evidence for the user-engine surface, including the
three registration-applicant fixtures (applicant allow, membership deny, wrong
issuer deny) replayed against the live `POST /v1/check`. The fail-closed
`FlexAuthHTTPAdapter` maps a missing or refused service to deny. This removes
authorization from this task's gate; only the credential handoff remains.
The latest tested image is published at
`forgejo.coulomb.social/coulomb/user-engine@sha256:a49a0a105d392e374f6da958f8ad1bff36f2cfa8779ac5cafa479b7f34b6efc1`;
the matching email-connect image is