Finish TEN-WP-0006-T04: expose guardrail read and write APIs
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
f631224ab5
commit
b6d016869f
5 changed files with 677 additions and 10 deletions
|
|
@ -235,7 +235,7 @@ absence — the one thing the contract forbids.
|
|||
|
||||
```task
|
||||
id: TEN-WP-0006-T04
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "4a256517-2773-4679-ad57-2909f22ac8a4"
|
||||
```
|
||||
|
|
@ -263,6 +263,47 @@ TEN-WP-0005-T05.
|
|||
Done when the routes are authorized, versioned, idempotent, correlated, and
|
||||
provider-neutral, and the OpenAPI document makes the semantics unambiguous.
|
||||
|
||||
Done 2026-08-16. Three routes: `GET /tenants/{id}/guardrails` (effective
|
||||
limits plus provenance), `PUT` and `DELETE
|
||||
/tenants/{id}/guardrails/{limit_key}`. 26 API tests; 227 total, all passing.
|
||||
Contract documented in `docs/tenant-guardrail-policy.md`.
|
||||
|
||||
**Required flex-auth actions — the tracked handoff this task exists to make
|
||||
explicit:**
|
||||
|
||||
| Action | Resource type | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `tenant.guardrail.read` | `guardrail` | read a tenant's effective ceilings |
|
||||
| `tenant.guardrail.set` | `guardrail` | set or clear a per-tenant override |
|
||||
|
||||
Both are **new** and need a policy-package change in `flex-auth` before this
|
||||
surface functions in production. Until then every check correctly resolves to
|
||||
deny. See the regression note below — these land on a package that is
|
||||
currently *behind*, not merely one that needs extending.
|
||||
|
||||
Decisions:
|
||||
|
||||
- **The read is authorized too**, unlike the existing `/roles` and
|
||||
`/roles/live` reads. A ceiling is policy about a tenant's commercial
|
||||
exposure, not a capability claim, and the task's whole point was that policy
|
||||
must be able to permit reading a limit without permitting a change. `actor`
|
||||
is a required query parameter, so there is no anonymous caller. Tested both
|
||||
ways: read-only permission cannot write, write-only permission cannot read.
|
||||
- **`amount` is a string on the wire.** It lets the `unlimited` sentinel and
|
||||
an integer share one field without a JSON union, and keeps a spend amount in
|
||||
minor units from ever round-tripping through a float.
|
||||
- **`change_id` is derived** from `(tenant_id, limit_key, Idempotency-Key)`
|
||||
rather than random, so a genuine retry replays the original audit record
|
||||
instead of minting a second one for a mutation that happened once.
|
||||
- **Authorization precedes every store touch**, so a denied read of a real
|
||||
tenant and of a nonexistent one are byte-identical — no probing existence or
|
||||
registry contents through status codes.
|
||||
- A retired tenant's refused loosening gets its own code,
|
||||
`guardrail_loosening_denied`, rather than reusing
|
||||
`invalid_lifecycle_transition`: the tenant's lifecycle is not in transition,
|
||||
and a caller should be able to tell "your tenant is retired, tighten instead"
|
||||
from "your retire/reactivate call was invalid".
|
||||
|
||||
## T05 - Conformance and consumer handoff
|
||||
|
||||
```task
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue