Finish TEN-WP-0010: mutable grouping, contract corrected, handoffs sent
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
2e1c4e15e6
commit
bdf9f5f643
4 changed files with 108 additions and 8 deletions
|
|
@ -63,6 +63,6 @@
|
|||
| task | TEN-WP-0009-T06 | todo | — | workplans/TEN-WP-0009-postgres-production-store.md |
|
||||
| task | TEN-WP-0010-T01 | todo | — | workplans/TEN-WP-0010-mutable-grouping.md |
|
||||
| task | TEN-WP-0010-T02 | done | — | workplans/TEN-WP-0010-mutable-grouping.md |
|
||||
| task | TEN-WP-0010-T03 | todo | — | workplans/TEN-WP-0010-mutable-grouping.md |
|
||||
| task | TEN-WP-0010-T04 | todo | — | workplans/TEN-WP-0010-mutable-grouping.md |
|
||||
| task | TEN-WP-0010-T03 | done | — | workplans/TEN-WP-0010-mutable-grouping.md |
|
||||
| task | TEN-WP-0010-T04 | done | — | workplans/TEN-WP-0010-mutable-grouping.md |
|
||||
| task | TEN-WP-0010-T05 | todo | — | workplans/TEN-WP-0010-mutable-grouping.md |
|
||||
|
|
|
|||
|
|
@ -145,6 +145,27 @@ deployment's canonical currency (`GUARDRAIL_CURRENCY`, default `EUR`).
|
|||
budget far faster than a human operator can notice, so its default ceiling is
|
||||
set for the blast radius, not the buying power.
|
||||
|
||||
### Grouping can change (TEN-WP-0010)
|
||||
|
||||
A tenant's grouping is **mutable**, through
|
||||
`POST /tenants/{id}/grouping` (action `tenant.grouping.set`). Reclassifying a
|
||||
tenant moves every grouping-derived ceiling with it — a `trial` tenant
|
||||
reclassified to `medium` goes from a zero spend budget to €1 000.
|
||||
|
||||
Two consequences for anyone reading guardrails:
|
||||
|
||||
- **A ceiling can change with no guardrail write against it.** The audit record
|
||||
is the `tenant_grouping_changed` event on the tenant, not an entry in the
|
||||
guardrail change log. That is deliberate: a guardrail trail containing
|
||||
entries no guardrail write produced would be worse than one that is complete
|
||||
but needs two event types read together.
|
||||
- **Precedence is unchanged.** A per-tenant override still wins over the new
|
||||
grouping default. Reclassification moves the grouping layer only.
|
||||
|
||||
Grouping is read from the tenant record, never parsed from the identifier's
|
||||
middle segment — that segment is historical, and the two are expected to
|
||||
diverge after a reclassification.
|
||||
|
||||
### Currency
|
||||
|
||||
Spend limits carry their currency explicitly. Because precedence resolves
|
||||
|
|
|
|||
|
|
@ -39,15 +39,60 @@ state — those stay exactly as retirement left them.
|
|||
|
||||
## Immutability
|
||||
|
||||
`tenant_id`, `identifier`, and `grouping` are immutable. The identifier is the
|
||||
IAM Profile `tenant` claim value that `key-cape` mints into tokens and
|
||||
`flex-auth` authorizes against; mutating it would silently invalidate every
|
||||
issued token that references it.
|
||||
`tenant_id` and `identifier` are immutable. The identifier is the IAM Profile
|
||||
`tenant` claim value that `key-cape` mints into tokens and `flex-auth`
|
||||
authorizes against; mutating it would silently invalidate every issued token
|
||||
that references it.
|
||||
|
||||
Mutable metadata is exactly: `display_name`, `contact_email`. Unknown fields
|
||||
are rejected by the request schema (`extra: forbid`), so the allow-list is
|
||||
visible in the OpenAPI document rather than discovered from a 400.
|
||||
|
||||
**`grouping` is mutable as of TEN-WP-0010**, but not through `PATCH` — see
|
||||
below. This document previously said it was immutable; that is no longer true.
|
||||
|
||||
---
|
||||
|
||||
## Reclassification (`grouping`)
|
||||
|
||||
### `POST /tenants/{tenant_id}/grouping`
|
||||
|
||||
```http
|
||||
POST /tenants/t-1/grouping
|
||||
Idempotency-Key: 4f1c…
|
||||
If-Match: "1"
|
||||
|
||||
{"grouping": "large", "actor": "ops",
|
||||
"reason": "grew past the band", "correlation_id": "corr-1"}
|
||||
```
|
||||
|
||||
Returns the full tenant record, `ETag`, and `Idempotent-Replay`, like the other
|
||||
mutations.
|
||||
|
||||
**Why this is not a `PATCH` field.** `display_name` and `contact_email` are
|
||||
cosmetic. `grouping` resolves spend ceilings (see
|
||||
`tenant-guardrail-policy.md`), so a reclassification moves money. It gets its
|
||||
own route, its own flex-auth action `tenant.grouping.set`, and its own audit
|
||||
event `tenant_grouping_changed`, so policy can permit a rename without
|
||||
permitting a reclassification and the audit trail shows which one happened.
|
||||
|
||||
**The identifier's grouping segment is historical.** A tenant created as
|
||||
`tenant:small:acme` and reclassified to `large` keeps the identifier
|
||||
`tenant:small:acme` and reports `"grouping": "large"`. The two are *expected*
|
||||
to diverge.
|
||||
|
||||
> **Do not parse grouping out of the identifier.** The segment records the
|
||||
> grouping at onboarding and is not authoritative afterwards. Read the
|
||||
> `grouping` field from this API. (ADR-0013 amendment proposed under
|
||||
> TEN-WP-0010; the identifier itself remains immutable either way.)
|
||||
|
||||
| Rule | Behaviour |
|
||||
|---|---|
|
||||
| unknown grouping | `400 invalid_grouping` |
|
||||
| no-op | `400 invalid_update` |
|
||||
| reserved tenant (`tenant:platform`, `tenant:coulomb`) | `400` — ungrouped by design; they resolve guardrails through the reserved profile |
|
||||
| retired tenant | `409 invalid_lifecycle_transition` — reactivate first |
|
||||
|
||||
---
|
||||
|
||||
## Endpoints
|
||||
|
|
@ -163,6 +208,7 @@ thereby granting a retirement:
|
|||
| `tenant.update` | `tenant` |
|
||||
| `tenant.retire` | `tenant` |
|
||||
| `tenant.reactivate` | `tenant` |
|
||||
| `tenant.grouping.set` | `tenant` |
|
||||
|
||||
These sit alongside the existing `tenant.create`, `tenant.role.grant`,
|
||||
`tenant.role.revoke`, and `tenant.plan.assign` actions in package
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ type: workplan
|
|||
title: "Make tenant grouping mutable and the identifier segment historical"
|
||||
domain: infotech
|
||||
repo: tenant-engine
|
||||
status: ready
|
||||
status: finished
|
||||
owner: claude
|
||||
topic_slug: tenant-guardrails
|
||||
created: "2026-08-17"
|
||||
|
|
@ -302,7 +302,7 @@ backend to diverge on.
|
|||
|
||||
```task
|
||||
id: TEN-WP-0010-T05
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "072cddc6-762a-4efc-8176-31176b9fded6"
|
||||
```
|
||||
|
|
@ -317,6 +317,39 @@ as part of this workplan.
|
|||
|
||||
Done when the contract is documented and the notes are sent.
|
||||
|
||||
Done 2026-08-17.
|
||||
|
||||
**Documentation.** `docs/tenant-lifecycle-api.md` gains a reclassification
|
||||
section — and a **correction**, because its Immutability section asserted
|
||||
`grouping` was immutable, which this workplan made false. Correcting a
|
||||
statement a consumer built against matters more than adding the new one.
|
||||
`docs/tenant-guardrail-policy.md` gains the consequence: a ceiling can now move
|
||||
with no guardrail write against it, and where the audit record lives.
|
||||
|
||||
**Handoffs sent.**
|
||||
|
||||
| To | Ask |
|
||||
| --- | --- |
|
||||
| `flex-auth` | add `tenant.grouping.set` (nine actions → ten) |
|
||||
| `adaptive-pricing` | grouping now moves; eligibility keyed on it has a transition to handle |
|
||||
| `user-engine` | contract correction, plus the new route for the operator UI |
|
||||
|
||||
Each carried the point most likely to bite that specific consumer rather than
|
||||
the same text three times. For `user-engine` that is the identifier/field
|
||||
divergence appearing in a **template** — our survey greps source confidently
|
||||
and templates much less so. For `adaptive-pricing` it is that a
|
||||
reclassification is a commercial event on their side even though it happened on
|
||||
ours, plus the standing offer on the plan-derived guardrail layer, which exists
|
||||
in the resolver and still has no feed. For `flex-auth` it is why this is not
|
||||
`tenant.update`: a subject permitted to rename must not thereby be permitted to
|
||||
raise a spend ceiling by two orders of magnitude.
|
||||
|
||||
Also told `adaptive-pricing` that the non-`trial` ceiling values remain
|
||||
unsigned-off, since a tier making claims that map to those numbers would
|
||||
otherwise inherit our provisional openers as though they were ratified.
|
||||
|
||||
Not deployed, per the task.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **Renaming tenants.** Not now, not as a fallback. The identifier stays
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue