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