ADORSYS-GIS / ADORSYS-GIS/lightbridge-authz
[Spike]: Phase 6a — make budget refills count at the gateway (EAIG × AuthConfig × ledger): decision memo
- Linguagem predominante
- Rust
- Estrelas
- 0
- Forks
- 1
- Merge médio
- 6h 42min
- PRs com merge (30d)
- 246
Descrição
Source of truth: https://github.com/ADORSYS-GIS/lightbridge-authz/issues/645 (admin console v2 epic; reset schedules #651 / PR #653) and `docs/governance-model-and-enforcement.md` Phase 6a/6b. Owner question (2026-09-03): *How do we connect our EAIG × AuthConfig to this so that refills become used, to allow more usage? Do we already have a mechanism?* Research memo below (read-only audit of lightbridge-authz, ai-helm, ai-helm-values at their 2026-09-03 heads; AI-drafted, citations verified by `sed -n`). Correction to §2 of the memo: prod runs `lightbridge-authz:sha-c3a3b6a` (ArgoCD `lightbridge-app` images, 2026-09-03), which already includes the reset scheduler (#653) — it is deployed, not pending.
# EAIG × AuthConfig × the budget ledger — does a refill buy more usage?
**Short answer: no.** Nothing at the gateway reads the ledger, and no rate-limit rule keys on
anything a refill changes. The one thing that *has* landed since the last time this was written
is that `budget_tier` **is** minted into the access token today (ADR-0014) — so the missing half
is no longer "get the tier into a claim", it is "Authorino stamps it and a BTP rule keys on it"
(Phase 6a), plus three defects that will bite the moment you wire it.
Everything below is verified against the live prod values files and the deployed image sha, not
against the docs — several of the docs are now stale, and that is called out where it matters.
---
## 1. Today's enforcement chain
### 1.1 The buckets that actually render in prod
| # | Rule | Descriptor keys | Limit source | Live? |
|---|---|---|---|---|
| 1 | Shared cross-model **monthly** budget | `x-account-id` (Distinct) + `x-billing-plan` (Exact) + `x-billing-period` (Distinct) | `backendTrafficPolicy.monthlyBudget.plans[].monthlyBudgetUsd` × 1e6 µUSD | **YES** |
| 2 | Shared cross-model **weekly** sub-budget | `x-account-id` (Distinct) + `x-billing-plan` (Exact) + `x-billing-week` (Distinct) | `plans[].weeklyBudgetUsd` × 1e6 | **YES** |
| 3 | per-model burst requests/min | `x-account-id` + `x-billing-plan` + `x-ai-eg-model` | `plans[].burst.requestsPerMin` | **no** — every `burst:` block commented out |
| 4 | per-model burst tokens/min | same | `plans[].burst.tokensPerMin` | **no** — same |
| 5 | per-model monthly budget | `x-account-id` + `x-billing-plan` + `x-billing-period` | `plans[].monthlyBudgetUsd` | **no** — dropped by `sharedBudget.enabled` |
| 6 | quota tiers (per member per project) | `x-project-id` + `x-account-id` (Distinct) + `x-quota-tier` (Exact) | `.Values.tiers[]` | **no** — `tiers: []` |
| 7 | project envelope | `x-project-id` (Distinct) + `x-project-quota` (Exact) | `.Values.projectEnvelope` | **no** — `projectEnvelope: {}` |
Citations:
- Rules 1–2 template: `/Users/selast/dev/gis/ai-helm/charts/core-gateway/templates/backendtrafficpolicy.yaml:101-133` (monthly) and `:149-184` (weekly). Both `shared: true`, `unit: Year` (a TTL only — ADR-0112 note at `:65-76`), `cost.response.from: Metadata` / `io.envoy.ai_gateway` / `llm_custom_total_cost` (`:124-132`).
- Live numbers: `/Users/selast/dev/gis/ai-helm-values/environments/prod/values/core-gateway.yaml:79-102` — `enabled: true`, and **all three plans are identical**: `enterprise` / `free` / `pro` each `monthlyBudgetUsd: 24`, `weeklyBudgetUsd: 6`. The plan dimension currently discriminates nothing but the Redis rule index.
- The `x-billing-period` / `x-billing-week` markers are stamped by a Lua `EnvoyExtensionPolicy`, not by Authorino: `/Users/selast/dev/gis/ai-helm/charts/core-gateway/templates/envoyextensionpolicy-billing-period.yaml:45-47` (`os.date("!%Y-%m")`, `os.date("!%G-W%V")`).
- Rules 3–7 template: `/Users/selast/dev/gis/ai-helm/charts/ai-model/templates/backendtrafficpolicy.yaml:176-267` (plan families) and `:273-399` (tier + envelope families).
- Rules 3–5 off in prod: `sharedBudget` on at `/Users/selast/dev/gis/ai-helm-values/environments/prod/values/models.yaml:91`; every `burst:` block commented at `models.yaml:146,156,168,174,181`.
- Rules 6–7 off in prod: `models.yaml:192` (`tiers: []`), `models.yaml:197` (`projectEnvelope: {}`).
**So exactly two buckets govern every request in prod, and both are keyed on
`(x-account-id, x-billing-plan, calendar-window)`.**
### 1.2 Where the header values come from
| Header | Expression | Origin |
|---|---|---|
| `x-account-id` | `auth.identity.iss == github ? repobinding.account_id : string(auth.identity.sub)` | the **JWT `sub`**, no introspection at all |
| `x-billing-plan` | github → repobinding; else `api_key_id` present → `lightbridgeintrospect.billing_plan` (else `"free"`); else `auth.identity.billing_plan` (else `"free"`) | introspection for anything api-key-shaped |
| `x-project-id` / `x-project-role` / `x-quota-tier` / `x-project-quota` | `has(auth.identity.api_key_id) ? lightbridgeintrospect. : auth.identity.` | introspection for anything api-key-shaped |
| `x-billing-period` / `x-billing-week` | not Authorino at all | Envoy Lua filter |
- `x-account-id`: `/Users/selast/dev/gis/ai-helm-values/environments/prod/values/security-policies.yaml:815-817`
- `x-billing-plan`: `security-policies.yaml:838-840`
- project group: `security-policies.yaml:881-892`
- **`x-budget-tier` does not exist.** `grep -rn 'budget.tier' ai-helm-values/` returns exactly one hit, in a prose comment (`ai-helm/charts/librechat-opencode-wellknown/values.yaml:234`) and the `ROADMAP.md` exit criterion (`ai-helm-values/ROADMAP.md:107`). No template, no AuthConfig, no BTP rule.
**Non-obvious, and load-bearing for the design in §4:** a token-exchange access token *does* carry
`api_key_id` (it is the `sessions.id` — `crates/lightbridge-authz-rest/src/signing.rs:446-460`), so the
human/OIDC plane **does** take the introspection branch of every expression above. This was a prod
incident on 2026-08-22 and the AuthConfig comment records the correction verbatim
(`security-policies.yaml:310-323`: *"The comment this replaced claimed 'human logins have no
api_key_id anyway' — FALSE"*). Introspection answers for exchange tokens through a dedicated arm
(`crates/lightbridge-authz-rest/src/handlers/introspect.rs:116-155`), returning `account_id`,
`project_id`, `billing_plan`, `quota_tier`, `project_quota` — **but no `budget_tier` field exists on
`IntrospectResponse` at all** (`introspect.rs:90-106` and `:137-153`).
⚠️ **ADR-0014 §Context is wrong on this point.** It argues the human plane "never reaches
introspection at all" because its first escape hatch is "no `api_key_id`"
(`docs/adr/0014-...md:36-42`). That was true when ADR-0011 was written and was corrected in prod six
days later. The ADR's *decision* still stands (minting the claim is fine); its *rationale for
rejecting introspection* does not, and that changes which of the two options in §4.1 is cheaper.
---
## 2. Is `budget_tier` minted? Stamped? Keyed on? Where is the runbook?
| Question | Answer | Evidence |
|---|---|---|
| Minted into tokens in prod? | **YES** | `crates/lightbridge-authz-rest/src/oauth2_op/store.rs:601,614` (exchange), `:1062,1077` (browser SSO), `:1724,1739` (refresh); resolver at `:819-840`. Deployed image `sha-1d7623ca3a27a90fa0d808ba0d1e90752104dbc9` (`ai-helm-values/environments/prod/values/lightbridge-app.yaml:8`) is commit 2026-09-02T10:10Z — well after ADR-0014 (2026-08-18). |
| Stamped as a header by Authorino? | **NO** | no `x-budget-tier` anywhere in `ai-helm` templates or `ai-helm-values` |
| Any BTP rule keyed on it? | **NO** | see §1.1 |
| Reachable via introspection (the API-key plane)? | **NO** | `IntrospectResponse` has no such field (`handlers/introspect.rs:90-106`) |
### Runbook status — `docs/runbooks/budget-tier-rekey-cutover.md`
| Runbook step | Status |
|---|---|
| §1 know the boundary (1st of month UTC) | **N/A / done** — ADR-0111 + ADR-0112 already made the boundary calendar-aligned; `x-billing-period` is live (`envoyextensionpolicy-billing-period.yaml:45-47`) |
| §2 same-PR exporter co-change | **PENDING** — `prometheus-redis-exporter.yaml:110-137` still hard-maps `rule-0=enterprise, rule-1=free, rule-2=pro` (monthly) and `rule-3/4/5` (weekly). Note the runbook's own text at `:49-52` is **stale**: it says the exporter scans `*rule-2-match-0*,*rule-7-match-0*`, which `prometheus-redis-exporter.yaml:55-61` records as already replaced by a wildcard. |
| §3 pre-deploy: every account has a tier claim | **half done** — the claim is minted for every exchange/refresh token (ADR-0014). Not true for the API-key plane, which has no path to it at all. |
| §3 pre-deploy: Authorino CEL has a default rung | **PENDING** — no CEL exists |
| §4 post-deploy verification | **PENDING** |
⚠️ Also stale and worth fixing while you are in there: `docs/budget-refill-ui-contract.md:233-269`
still says *"no code path anywhere in this repo writes a budget-tier claim"* and describes Phase 6b
as "write the granted tier back to Keycloak". Both were superseded by ADR-0014 four months of commits
ago (`git log` on that file stops at #412). Its **conclusion** — a refill has no gateway effect — is
still correct, for a different reason than the one it gives.
---
## 3. What a refill actually changes today
```
requestBudgetRefill / grantBudget / a reset schedule
│
├─► budget_grants (immutable ledger row, ADR-0009) ✅ changes
├─► budget_balances (materialized projection) ✅ changes
├─► budget_tier claim on the NEXT token mint/refresh ✅ changes (ADR-0014) — but see the two defects below
├─► console display (getMyBudgetRefillLadder / refill_status) ✅ changes
│
├─► x-budget-tier header ❌ header does not exist
├─► x-quota-tier / x-project-quota ❌ untouched (governance doc §5, lines 540-552)
├─► any Envoy ratelimit descriptor ❌ none key on anything a refill moves
└─► what the user may actually spend ❌ UNCHANGED — still $24/month, $6/week, per plan
```
The claim moves; nothing consumes the claim. `governance-model-and-enforcement.md:540-552` states this
directly, and it is still accurate.
### Two defects that already make the minted claim untrustworthy
**(a) Any non-ladder grant silently collapses the claim to `b-15`.**
`BudgetRepo::current_tier` reads the **single most recent** tier-representing grant's `amount_micros`
and maps it through `BudgetTier::from_amount_micros`, falling back to `B15` on no match
(`crates/lightbridge-authz-budget/src/repo.rs:551-571`; SQL at `:136-139`, sources
`base|self_service|automatic|admin|manual_approval|promotion`). But ADR-0015 made refill amounts
**admin-configured policy ranges**, not the seven compile-time rungs. So a $40 grant → no rung →
claim reads `b-15`, i.e. a refill can *lower* the claim. `current_tier`'s own doc comment flags this
as a "known simplification" (`repo.rs:532-536`) — it becomes a live enforcement bug the moment §4 lands.
**(b) Reset schedules (PR #653, ADR-0032) make (a) systematic.** A `reset` writes a grant whose
amount is `delta = target − (effective_budget − spend_to_date)` (ADR-0032 D2) — an arbitrary number
that will essentially never equal a rung, with `source = 'automatic'`, which **is** in the
tier-representing source list. So every daily reset re-pins that account's claim to `b-15`. This is
latent today (nothing reads the claim) and is not deployed either: PR #653 merged 2026-09-02T19:21Z,
nine hours *after* the commit behind the running image.
**(c) The fail-closed floor emits a label no rung will ever match.** `resolve_budget_tier` downgrades
any ledger outage to `budget_tier_wire_label(fail_closed_floor_micros)`
(`store.rs:819-840`), and the shipped default floor is $6
(`crates/lightbridge-authz-budget/src/rule_data.rs:148`) → the claim reads **`"b-6"`**
(`store.rs:107-112`). Against an `Exact` descriptor selector, `b-6` matches **no rule** — and a
request matching no budget rule is **unlimited**, not throttled. A fail-*closed* code path that
produces a fail-*open* gateway outcome. This is precisely the failure the runbook warns about at
`docs/runbooks/budget-tier-rekey-cutover.md:69-71`.
---
## 4. The shortest correct path
### 4.1 Where to source `x-budget-tier` — pick one, and it is not obvious
**Option A — from the JWT claim (`auth.identity.budget_tier`).** Zero authz-side work; the claim
already exists. But: frozen at mint time (a refill lands on next refresh, minutes to hours), and
**the API-key plane gets nothing** — every LibreChat / CI / SA caller would fall to the default rung.
Given ADR-0014's introspection rationale is now factually wrong (§1.2), "claims are the only option"
is no longer true.
**Option B — add `budget_tier` to `IntrospectResponse`.** One field on
`crates/lightbridge-authz-rest/src/handlers/introspect.rs` (both arms, `:90-106` and `:137-153`) plus
a `BudgetRepo::current_tier` call. Covers **both planes**, and it is **live within 30 s** (Authorino's
introspection cache) instead of frozen until refresh — which is exactly the property
`governance-model-and-enforcement.md:272-290` says is worth paying introspection for. Costs one extra
DB read inside the already-cached introspection call. **This is the better option**, and it makes the
product message "your refill is active within a minute" instead of "sign out and back in".
Either way the CEL must mirror the existing precedence shape (introspection wins, claim second,
default third) and, critically, must **never** emit a value that matches no rule:
```yaml
"x-budget-tier":
plain:
expression: >
(has(auth.metadata) && has(auth.metadata.lightbridgeintrospect)
&& has(auth.metadata.lightbridgeintrospect.budget_tier)
&& auth.metadata.lightbridgeintrospect.budget_tier != "")
? string(auth.metadata.lightbridgeintrospect.budget_tier)
: ((has(auth.identity.budget_tier) && auth.identity.budget_tier != "")
? string(auth.identity.budget_tier)
: "b-15")
```
The `"b-15"` tail is not cosmetic — it is the difference between "base budget" and "unlimited"
(runbook `:69-71`). Every level `has()`-guarded under `&&` short-circuit, per the ADR-0047/0052
dropped-header incident (`security-policies.yaml:876-880`, `:922-936`).
### 4.2 Then key a rule on it
Append a tier family to the **gateway-wide** BTP (that is where `shared: true` collapses the
cross-model counter — `core-gateway/templates/backendtrafficpolicy.yaml:44-52`), strictly **after**
the existing plan loops so no `rule/N` index shifts (ADR-0084, `:93-98`):
```yaml
- clientSelectors:
- headers:
- {invert: false, name: x-account-id, type: Distinct}
- {invert: false, name: x-budget-tier, type: Exact, value: "b-15"}
- {invert: false, name: x-billing-period, type: Distinct}
limit: {requests: 15000000, unit: Year} # unit is a TTL only — ADR-0112
shared: true
cost: {request: {from: Number, number: 0},
response: {from: Metadata, metadata: {namespace: io.envoy.ai_gateway, key: llm_custom_total_cost}}}
```
…one per rung from `crates/lightbridge-authz-budget/src/tier.rs:29-62`
(`b-15/30/60/120/250/500/1000` → 15e6 … 1000e6 µUSD, `tier.rs:39-50`).
**Do NOT retire the per-plan rules in the same PR.** The plan rules and the tier rules compose with
AND (Envoy denies if any matched bucket is exhausted), so leaving both in place means the effective
cap is `min(plan, tier)` — and with all three plans at $24 today
(`core-gateway.yaml:89,97,101`) the plan rule is a safety net while the tier path is proven. Retire
the plan rules in a **second** PR, on a 1st-of-month boundary. This deviates from the runbook's
one-shot cutover, and deliberately: the runbook was written when the plan rule was the only bucket
and dropping it was unavoidable.
### 4.3 The window problem, and reset schedules
The gateway window is **not** configurable per-account. It is a calendar `YYYY-MM` string stamped by
a Lua filter (`envoyextensionpolicy-billing-period.yaml:46`) folded into the Redis key, with
`unit: Year` pinning the epoch so the marker is the sole rotation trigger (ADR-0112,
`core-gateway/templates/backendtrafficpolicy.yaml:65-76`).
**A daily reset schedule cannot be expressed as a rate-limit bucket.** ADR-0032's "reset remaining
to $2 every day at 00:00 UTC" writes ledger rows; the gateway counter keeps accumulating inside the
current `2026-09` bucket regardless. Two escapes, both real:
1. **Add a daily descriptor.** The same Lua block can stamp `x-billing-day` (`os.date("!%F")`) and a
third rule family keys on it — mechanically identical to the ADR-0119 weekly rule
(`:134-184`), which is the existence proof that this shape works. Composes (AND) with monthly, so
it can only ever *tighten*. This gets you "daily pacing", **not** "reset to $2/day": the daily
bucket is a fixed per-tier number, not a ledger balance. For the owner's literal ask ($2/day for
free plan) that is functionally equivalent and costs one values change.
2. **Dynamic per-request balance check** (option C in `ai-helm/plans/lightbridge-dynamic-budget.md:42`).
A component in the data path reads remaining from the ledger per request. **Recommend against for
now**, on the plan's own evidence: it puts a Postgres read back into the ext_authz hot path —
exactly the shape disabled on 2026-07-02 after the ext_authz timeout turned a slow dependency into
fail-open on every request (`plans/lightbridge-dynamic-budget.md:229-241`). It also cannot do
reserve-and-settle without a pre-request cost estimate, because `llm_custom_total_cost` is only
known *after* the response (`:243-250`). It retires ADR-0021/0084/0110's machinery wholesale and
is its own project, not a phase.
Verdict: **(1) now, (2) never as an ext_authz step — only ever as its own ext_proc component with its
own spike.**
### 4.4 PRs, in order
| # | Repo | Change | Risk |
|---|---|---|---|
| 1 | `lightbridge-authz` | Fix `current_tier` so a non-rung amount resolves to the **highest rung ≤ amount** instead of `B15` (`repo.rs:551-571`), and make the fail-closed floor emit a real rung, not `b-6` (`store.rs:819-840`, `rule_data.rs:148`) | none at the gateway (nothing reads it yet). **Must land before anything downstream keys on the claim.** |
| 2 | `lightbridge-authz` | Add `budget_tier` to `IntrospectResponse`, both arms (`handlers/introspect.rs:90-106`, `:137-153`) | additive field; Authorino ignores unknown fields |
| 3 | `ai-helm` | Template the tier rule family on the gateway-wide BTP, gated on a new `budgetTiers` values list (empty ⇒ renders nothing) | none while the list is empty |
| 4 | `ai-helm-values` | (a) `x-budget-tier` CEL on **both** AuthConfigs — real expression on `main`, constant `""` on `internal`; (b) populate `budgetTiers`; (c) exporter `CHECK_KEYS` + ServiceMonitor relabelings + `ratelimit_quota.py` in the **same** PR | **HIGH — see below** |
| 5 | `ai-helm-values` | Retire the per-plan monthly rules, on a 1st-of-month boundary | high, but this is the ADR-0084 mechanism used deliberately |
**Risk, stated plainly.** A wrong descriptor here is bidirectional and both directions are bad:
- A tier value that matches **no** `Exact` selector (`b-6`, `b-40`, `""`) ⇒ that rule family never
matches ⇒ **that account is unlimited** on the tier axis. This is why PR #1 comes first and why the
CEL default rung is mandatory.
- A **dropped** header (an un-`has()`-guarded CEL that errors) ⇒ Authorino omits it ⇒ Envoy's
ext_authz leaves the **client-supplied** value intact ⇒ a caller can pick their own budget tier.
The AuthConfig documents this anti-spoofing rule at `security-policies.yaml:922-936`, which is also
why the internal AuthConfig must stamp a constant `""` (`:1217-1229` does exactly this for
`x-quota-tier`).
- Any **reorder** of the rendered rule list ⇒ every account's mid-window spend is orphaned and the
fleet gets a fresh budget (the 2026-07-16 incident, ADR-0084 — `core-gateway.yaml:62-71`).
Append-only, always.
### 4.5 The identity mismatch nobody has hit yet
`x-account-id` is `auth.identity.sub` (`security-policies.yaml:817`). The ledger is keyed on
`budget_account_id` = `context.account_id` (`store.rs:601`), and the token carries **both**: `sub`
comes from `identity_for(&owner)` where `owner.account_id = subject`
(`crates/lightbridge-authz-rest/src/oauth2_op/store.rs:592-594`, `signing.rs:430-436`), while the
`account_id` claim is `context.account_id` (`signing.rs:499-502`).
Today these are the same string, because `accounts.id` **is** the subject
(`docs/adr/0026-...md:27-28`). **ADR-0026 is `Status: Proposed`** (`:3`) and changes exactly that. The
day it lands, one identity owns many accounts, the ledger meters per account, and the gateway counter
is still per `sub` — so all of a person's accounts share one gateway budget while the console shows
them separate balances. Fix it in PR #4 by keying the tier rules on a new `x-budget-account-id`
(stamped from the `account_id` claim / introspection `account_id`), not on `x-account-id`.
### 4.6 Diagrams
```mermaid
sequenceDiagram
autonumber
participant U as User / client
participant IDP as authz-idp
TokenExchangeOpStore
participant LED as budget ledger
budget_grants / budget_balances
participant AZ as Authorino
(ext_authz, filter #7)
participant INTRO as authz-api
/v1/authorino/validate/introspect
participant RL as Envoy ratelimit (Lyft)
Redis counters
participant M as model backend
U->>IDP: POST /oauth2/token (exchange | refresh)
IDP->>LED: current_tier(account_id, Period::current)
repo.rs:551-571
LED-->>IDP: BudgetTier (or Err -> floor, store.rs:819-840)
IDP-->>U: access_token { sub, account_id, budget_tier, quota_tier, … }
store.rs:601,614
U->>AZ: POST /v1/chat/completions (Bearer)
AZ->>INTRO: introspect (api_key_id present -> ALWAYS, incl. exchange tokens)
security-policies.yaml:310-323
INTRO-->>AZ: {active, account_id, project_id, billing_plan, quota_tier, …}
handlers/introspect.rs:137-153
Note over AZ,INTRO: ❌ no budget_tier on this response today
AZ-->>AZ: stamp x-account-id (=sub), x-billing-plan,
x-project-*, x-quota-tier
security-policies.yaml:815-892
Note over AZ: ❌ x-budget-tier is never stamped
AZ->>RL: descriptors (x-account-id, x-billing-plan, x-billing-period/-week)
alt monthly OR weekly bucket exhausted
RL-->>U: 429
else within budget
RL->>M: forward
M-->>RL: response
RL->>RL: charge llm_custom_total_cost µUSD
backendtrafficpolicy.yaml:124-132
RL-->>U: 200
end
```
```mermaid
stateDiagram-v2
direction LR
state "LEDGER — per (account, YYYY-MM)" as L {
[*] --> NoGrant: new period
NoGrant --> AtRung: grant matches a rung
tier.rs:81-92
NoGrant --> OffLadder: grant is an arbitrary policy amount
ADR-0015
AtRung --> AtRung: refill -> next() rung
tier.rs:64-74
AtRung --> OffLadder: reset-schedule delta grant
ADR-0032 D2 (source='automatic')
OffLadder --> OffLadder: every further reset
NoGrant --> Floor: ledger unreachable at mint
store.rs:827-838
}
state "CLAIM (minted, ADR-0014)" as C {
AtRung --> claim_rung: budget_tier = "b-NNN"
OffLadder --> claim_b15: budget_tier = "b-15" ⚠ collapse
repo.rs:565-570
Floor --> claim_b6: budget_tier = "b-6" ⚠ off-ladder
rule_data.rs:148
}
state "GATEWAY — per (x-account-id, x-billing-plan, YYYY-MM)" as G {
claim_rung --> Ignored
claim_b15 --> Ignored
claim_b6 --> Ignored
Ignored: no header, no rule reads it
Ignored --> PlanBucket: only bucket that exists
PlanBucket: $24/month, $6/week — identical for all 3 plans
core-gateway.yaml:89,97,101
PlanBucket --> Exhausted429
PlanBucket --> Allowed200
Exhausted429 --> PlanBucket: x-billing-period flips on the 1st
envoyextensionpolicy-billing-period.yaml:46
}
note right of Ignored
AFTER Phase 6a this edge becomes
claim -> x-budget-tier -> TierBucket.
An off-ladder label ("b-6", "b-40") would
then match NO Exact selector => UNLIMITED.
Runbook §3, budget-tier-rekey-cutover.md:69-71
end note
```
---
## 5. Owner decisions
1. **Claim or introspection as the source of `x-budget-tier`?** Claim = zero authz work but
human-plane-only and stale until refresh. Introspection = one field, both planes, live in 30 s.
ADR-0014's reason for rejecting introspection is factually obsolete (§1.2); does the ADR get
amended, or does the API-key plane stay permanently outside refills (as ADR-0008 §0.1 decided)?
2. **Tier ladder ↔ limit mapping.** `tier.rs` says `b-15 = $15`. Prod says every plan gets **$24**.
So the base rung is *larger* than the current cap on the free plan and *smaller* than what
enterprise pays for. Does `b-15` mean $15 (a cut for everyone) or does the ladder get re-anchored
at today's $24? And does plan still determine the starting rung — there is no `billing_plan →
BudgetTier` mapping anywhere in the codebase (`repo.rs:532-536`).
3. **Off-ladder amounts.** ADR-0015 allows arbitrary policy amounts; the gateway needs a finite set
of `Exact` values. Round **down to the nearest rung** (safe, under-grants), round **up** (over-grants),
or **constrain policy** so configured amounts must be rungs? This is the single highest-risk
decision — the wrong answer means "unlimited".
4. **Window semantics.** Do refills reset the calendar-month bucket, or is a daily/weekly pacing
descriptor added alongside (the ADR-0119 shape)? A daily *reset-to-$2* is not expressible as a
ratelimit bucket at all — is "a $2 daily pacing bucket" an acceptable substitute?
5. **Keep or retire the per-plan rules?** Recommendation is to run both (AND ⇒ `min`) for one month,
contradicting the runbook's single-cutover. Confirm.
6. **Counter identity — `sub` or `account_id`?** ADR-0026 (Proposed) makes these diverge. Key the tier
rules on a new `x-budget-account-id` now, or accept that all of a person's accounts share one
gateway counter?
7. **Reset schedules vs. the tier claim.** ADR-0032 `automatic` grants are tier-representing
(`repo.rs:136-139`) and will pin the claim to `b-15`. Exclude `automatic` from the tier-source
list, or fix the mapping per decision 3?
8. **Cutover date.** Next boundaries: 2026-10-01, 2026-11-01, 00:00 UTC. PR #653 is merged but **not
deployed** (image `sha-1d7623ca`, 2026-09-02T10:10Z, precedes the #653 merge at 19:21Z) — does the
reset-schedule rollout precede or follow 6a?
Guia de contribuição
Avaliação
Esta issue ainda não foi avaliada.