Finish TEN-WP-0006-T03: persist guardrails in both stores
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
33ceb882ee
commit
f631224ab5
7 changed files with 701 additions and 3 deletions
|
|
@ -168,7 +168,7 @@ folded into this diff.
|
|||
|
||||
```task
|
||||
id: TEN-WP-0006-T03
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "f1c93573-6322-4505-b738-7d66d67e60a8"
|
||||
```
|
||||
|
|
@ -191,6 +191,46 @@ Done when the store-conformance suite (already parametrised over both backends)
|
|||
covers guardrails, so the durable store cannot diverge from the reference
|
||||
semantics.
|
||||
|
||||
Done 2026-08-16: `TenantStore` gains `guardrail_overrides`, `guardrail_changes`,
|
||||
and `set_guardrail_override`, implemented in both backends.
|
||||
`tests/test_guardrail_store_conformance.py` parametrises 35 tests over
|
||||
in-memory and SQLite. 201 tests total, all passing.
|
||||
|
||||
Deviations and decisions, both deliberate:
|
||||
|
||||
- **No backfill migration, because grouping defaults are resolved rather than
|
||||
materialised.** The task anticipated backfilling existing tenants with their
|
||||
grouping-derived defaults. Storing them would have been worse: it duplicates
|
||||
a value that already has a single source, and it makes changing a default a
|
||||
data migration instead of a config edit. Since a tenant with no override
|
||||
resolves through the same function whether it predates guardrails or not,
|
||||
"existing tenants must not silently gain a looser limit than a fresh tenant
|
||||
of the same grouping" holds by construction, not by a migration step that
|
||||
could be skipped. The forward-only, idempotent part is the two new tables,
|
||||
created by the same `CREATE TABLE IF NOT EXISTS` script fresh and existing
|
||||
databases both take — no second schema definition to drift.
|
||||
- **A guardrail change bumps the tenant version.** It reuses the lifecycle
|
||||
ETag, so a guardrail write invalidates a stale reader's `If-Match` exactly
|
||||
as a metadata edit does, rather than introducing a second, separately
|
||||
versioned resource that a caller could race against the first.
|
||||
|
||||
Retired tenants, following the TEN-WP-0005 reduce-privilege precedent:
|
||||
|
||||
- Guardrails stay **readable**; it is the values that clamp, not the endpoint.
|
||||
- A retired tenant's effective limits clamp to the floor, provenance
|
||||
`lifecycle`.
|
||||
- **Tightening is allowed while retired, loosening is refused.** The check
|
||||
compares the before/after value computed *as if the tenant were active* —
|
||||
because the clamp makes every candidate look inert today, and the real
|
||||
effect of a loosening override lands the moment the tenant is reactivated.
|
||||
That deferred loosening is the thing worth refusing. Clearing an override is
|
||||
evaluated the same way, so clearing a tightening override on a retired
|
||||
tenant is refused too, since fall-through would loosen.
|
||||
|
||||
Stored amounts are `TEXT NOT NULL` with an explicit `"unlimited"` token, never
|
||||
NULL. A NULL that meant unlimited would make an open ceiling the result of
|
||||
absence — the one thing the contract forbids.
|
||||
|
||||
## T04 - Expose guardrail read and write APIs
|
||||
|
||||
```task
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue