Plan guardrail and quota policy (TEN-WP-0006)
Closes the guardrail/quota concern reserved since repo creation. Notably ADR-0013 specifies trial tenants default to a zero spend budget; no such default exists today. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
77bc207484
commit
b8af02d74d
1 changed files with 186 additions and 0 deletions
186
workplans/TEN-WP-0006-guardrail-quota-policy.md
Normal file
186
workplans/TEN-WP-0006-guardrail-quota-policy.md
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
---
|
||||||
|
id: TEN-WP-0006
|
||||||
|
type: workplan
|
||||||
|
title: "Guardrail and quota policy for tenants"
|
||||||
|
domain: infotech
|
||||||
|
repo: tenant-engine
|
||||||
|
status: ready
|
||||||
|
owner: claude
|
||||||
|
topic_slug: tenant-guardrails
|
||||||
|
created: "2026-08-16"
|
||||||
|
updated: "2026-08-16"
|
||||||
|
depends_on:
|
||||||
|
- TEN-WP-0005
|
||||||
|
unblocks: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# TEN-WP-0006 - Guardrail and quota policy
|
||||||
|
|
||||||
|
Implement the guardrail/quota concern that `SCOPE.md`, `INTENT.md`, and
|
||||||
|
`.claude/rules/architecture.md` have reserved since the repo was created but
|
||||||
|
never built. `tenant-engine` owns *policy* — what a tenant is permitted to
|
||||||
|
spend and how many entities/actions it may hold — not metering, not billing,
|
||||||
|
and not the enforcement decision itself.
|
||||||
|
|
||||||
|
The concrete hole this closes: ADR-0013 specifies that `trial`-grouped tenants
|
||||||
|
default to a **zero** spend budget. No such default exists today, so every
|
||||||
|
trial tenant currently carries no ceiling at all.
|
||||||
|
|
||||||
|
## Boundary constraints
|
||||||
|
|
||||||
|
These are not negotiable without a new ADR, and every task below is bound by
|
||||||
|
them:
|
||||||
|
|
||||||
|
- **`tenant-engine` is a data source, never a PDP.** It answers "what is this
|
||||||
|
tenant's limit" and "what is its recorded consumption". `flex-auth` decides
|
||||||
|
whether an action is allowed. Never invert this.
|
||||||
|
- **No metering.** Consumption counters are written *to* this service by
|
||||||
|
whoever meters; `tenant-engine` does not observe spend or count actions
|
||||||
|
itself.
|
||||||
|
- **No plan terms.** A guardrail may be *derived from* an `adaptive-pricing`
|
||||||
|
plan id, but plan definitions are never copied here.
|
||||||
|
- **Fail closed.** An unknown tenant, an unreadable store, or a missing
|
||||||
|
guardrail resolves to the most restrictive answer, never to "unlimited".
|
||||||
|
|
||||||
|
## T01 - Specify the guardrail model and its boundary
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: TEN-WP-0006-T01
|
||||||
|
status: todo
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Write the guardrail domain contract before any code. Decide and document:
|
||||||
|
|
||||||
|
- The **limit kinds** in scope. Start with spend budget (a currency amount
|
||||||
|
over a period) and entity/action counts; leave rate limiting out unless it
|
||||||
|
falls out for free.
|
||||||
|
- **Precedence.** A tenant may inherit a limit from its grouping (ADR-0013),
|
||||||
|
from its assigned plan, and from an explicit per-tenant override. Define the
|
||||||
|
resolution order and make it total — every tenant resolves to exactly one
|
||||||
|
effective guardrail set, with no ambiguity and no "unset means unlimited".
|
||||||
|
- **The `trial` = zero-spend default**, and what the default is for each other
|
||||||
|
grouping. An unmapped grouping must resolve restrictively, not openly.
|
||||||
|
- **Reserved identifiers.** `tenant:platform` and `tenant:coulomb` are
|
||||||
|
ungrouped (see `domain.py`); state explicitly what they resolve to.
|
||||||
|
- Whether consumption lives in this repo at all. If it does, it is an opaque
|
||||||
|
counter written by an external meter, with the writer named in the contract.
|
||||||
|
|
||||||
|
Document the consumer-facing shape in `docs/tenant-guardrail-policy.md`,
|
||||||
|
matching the style of `docs/tenant-lifecycle-api.md`.
|
||||||
|
|
||||||
|
Done when the precedence rules are unambiguous, the fail-closed default is
|
||||||
|
explicit for every grouping, and the doc states plainly what this service does
|
||||||
|
*not* do (meter, bill, decide).
|
||||||
|
|
||||||
|
## T02 - Implement the guardrail domain model
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: TEN-WP-0006-T02
|
||||||
|
status: todo
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a `guardrail/` module (the namespace `architecture.md` reserves) holding
|
||||||
|
pure domain types with no framework dependency, mirroring how `domain.py` is
|
||||||
|
structured: frozen dataclasses, validation that raises typed errors, and
|
||||||
|
resolution logic as a pure function over (grouping, plan_id, overrides).
|
||||||
|
|
||||||
|
Guardrail changes are **audited like role grants** — append-only, carrying
|
||||||
|
actor, reason, and correlation id. A limit change is a privilege change and
|
||||||
|
must be as reconstructible as a `RoleGrant`.
|
||||||
|
|
||||||
|
Done when effective-guardrail resolution is a total, side-effect-free function
|
||||||
|
with unit tests covering every grouping, the reserved identifiers, missing
|
||||||
|
plans, and conflicting overrides.
|
||||||
|
|
||||||
|
## T03 - Persist guardrails in both stores
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: TEN-WP-0006-T03
|
||||||
|
status: todo
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Extend the `TenantStore` Protocol and both implementations (`InMemoryTenantStore`
|
||||||
|
and the SQLite store) with guardrail persistence and its audit trail. Follow the
|
||||||
|
precedent set by `mutate_tenant()` in TEN-WP-0005: the mutation, the audit
|
||||||
|
event, and any idempotency receipt commit **in one transaction** — a crash must
|
||||||
|
never leave a changed limit with no audit record.
|
||||||
|
|
||||||
|
Add a forward-only, idempotent migration that backfills existing tenants with
|
||||||
|
their grouping-derived defaults. Existing tenants must not silently gain a
|
||||||
|
looser limit than a fresh tenant of the same grouping would get.
|
||||||
|
|
||||||
|
Interaction with lifecycle: decide and test what a `retired` tenant's
|
||||||
|
guardrails resolve to. Follow the TEN-WP-0005 precedent — operations that only
|
||||||
|
*reduce* privilege stay available while retired; loosening does not.
|
||||||
|
|
||||||
|
Done when the store-conformance suite (already parametrised over both backends)
|
||||||
|
covers guardrails, so the durable store cannot diverge from the reference
|
||||||
|
semantics.
|
||||||
|
|
||||||
|
## T04 - Expose guardrail read and write APIs
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: TEN-WP-0006-T04
|
||||||
|
status: todo
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the API surface, authorized through the existing flex-auth `WriteAuthorizer`
|
||||||
|
with **distinct actions** so policy can permit reading a limit without
|
||||||
|
permitting a change to it — the same split TEN-WP-0005 used for
|
||||||
|
`tenant.update` / `tenant.retire` / `tenant.reactivate`:
|
||||||
|
|
||||||
|
- a read returning a tenant's effective guardrails plus their provenance
|
||||||
|
(which layer each limit came from), for `flex-auth` to consume;
|
||||||
|
- a write to set or clear a per-tenant override, requiring `Idempotency-Key`,
|
||||||
|
`If-Match`, actor, reason, and correlation id, consistent with the lifecycle
|
||||||
|
mutation contract.
|
||||||
|
|
||||||
|
Authorization runs **before** the store is touched, so an unauthorized caller
|
||||||
|
cannot probe which tenants exist. Errors are redacted: never reflect policy
|
||||||
|
internals or store paths in `detail`.
|
||||||
|
|
||||||
|
New flex-auth actions mean a policy-package change outside this repo. Name the
|
||||||
|
required actions explicitly in the task record so the flex-auth side is a
|
||||||
|
tracked handoff and not a surprise — this is exactly what stalled
|
||||||
|
TEN-WP-0005-T05.
|
||||||
|
|
||||||
|
Done when the routes are authorized, versioned, idempotent, correlated, and
|
||||||
|
provider-neutral, and the OpenAPI document makes the semantics unambiguous.
|
||||||
|
|
||||||
|
## T05 - Conformance and consumer handoff
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: TEN-WP-0006-T05
|
||||||
|
status: todo
|
||||||
|
priority: medium
|
||||||
|
```
|
||||||
|
|
||||||
|
Cover: every grouping's default, `trial` resolving to zero spend, precedence
|
||||||
|
between grouping/plan/override, unmapped grouping failing closed, reserved
|
||||||
|
identifiers, guardrail reads on a retired tenant, stale-version conflict,
|
||||||
|
idempotent replay, cross-tenant authorization denial, store outage, and error
|
||||||
|
redaction. Prove the existing create/role/plan/lifecycle clients are unaffected.
|
||||||
|
|
||||||
|
Then write the consumer handoff for `flex-auth` — the guardrail read is only
|
||||||
|
useful once a PDP consults it — naming the API version, the authorization
|
||||||
|
actions, and the contract doc, in the table format TEN-WP-0005-T05 used.
|
||||||
|
|
||||||
|
**Do not** deploy to production as part of this workplan. Cluster rollout is a
|
||||||
|
separate, human-authorized step; see the note below.
|
||||||
|
|
||||||
|
Done when the full suite passes and the handoff is sent.
|
||||||
|
|
||||||
|
## Out of scope / explicitly deferred
|
||||||
|
|
||||||
|
- **Production rollout.** Image build and cluster apply need credentials and a
|
||||||
|
human authorization decision. Track separately.
|
||||||
|
- **The live flex-auth regression** (`flex-auth-tenant-engine` rolled back to
|
||||||
|
the four-action image, so `tenant.update` / `tenant.retire` /
|
||||||
|
`tenant.reactivate` currently deny with `unknown_action`). Unrelated to this
|
||||||
|
workplan, but it means any *new* actions from T04 land on a policy package
|
||||||
|
that is already behind. Resolve the regression before shipping T04's actions.
|
||||||
|
- **Metering and billing.** Not this repo, at any point.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue