Repository navigation
Description
Context / Why
Issue #485 demoted the account daily circuit breakers to observe-only: the two ceiling checks in reserve() (delegation-budget-store.ts:305,319) are now gated on isBudgetEnforced(env), which returns false by default (BUDGET_ENFORCE is unset in production). Enforcement returns at paid launch.
A new API was shipped in the same window: GET /PUT /v1/budget/limits (workers/api/src/routes/budget.ts, registered at index.ts:168). It lets a signed-in user read and write their per-account daily ceilings (charged-micros and token). The response includes resolved effective values, 24h consumption, distance-to-limit, and tier provenance — but contains no signal that enforcement is currently off.
The consequence: a user who sets a "I want a $40/day cap" via PUT /v1/budget/limits will see it stored and echoed back with effective.chargedMicrosCeiling matching their intent. Nothing in the response tells them the ceiling will not block a single run. This is the same silent no-op failure pattern called out in #463, applied to a safety control instead of a behavior field.
The fix has a clear precedent: GET /v1/billing/status (routes/billing.ts:37–43) returns an enforced boolean sourced from isPaywallEnforced(c.env). isBudgetEnforced(env) is the exact equivalent for the budget breakers.
Scope
In scope:
- Add
enforced: boolean(result ofisBudgetEnforced(c.env)) to the response body of bothGETandPUT /v1/budget/limits. - Update the OpenAPI schema (
store/openapi.yaml, lines 8084–8117 for GET, line 8150 for PUT) to includeenforcedwith a description that explains what it means (ceilings measured but never blocking whenfalse). - When
enforcedisfalse, the API description on PUT must make clear the stored values are configuration for future enforcement, not a live gate. - When [frontend] Budget & limits settings surface — show 24h consumption vs each ceiling + distance-to-limit, and edit the limits (console owner + admin) #475's console Budget UI panel is built on top of this API, it must read
enforcedand render an observe-only notice when it isfalse(the same note [decision] Demote AI budget circuit breakers to observe-only (BUDGET_ENFORCE, default off) — keep structural caps + meter; re-enable at paid launch #485 mandated on the Usage page, but applied to the limits settings surface). Link this requirement back to [frontend] Budget & limits settings surface — show 24h consumption vs each ceiling + distance-to-limit, and edit the limits (console owner + admin) #475 so it is not dropped.
Out of scope:
- Changing the enforcement logic itself — that is [decision] Demote AI budget circuit breakers to observe-only (BUDGET_ENFORCE, default off) — keep structural caps + meter; re-enable at paid launch #485's decision.
- Adding
enforcedto any other route (Usage page already has an inline note per [decision] Demote AI budget circuit breakers to observe-only (BUDGET_ENFORCE, default off) — keep structural caps + meter; re-enable at paid launch #485; this is only the budget/limits surface). - Any UI work — this is a backend ticket; the UI carry-on belongs to [frontend] Budget & limits settings surface — show 24h consumption vs each ceiling + distance-to-limit, and edit the limits (console owner + admin) #475.
Acceptance criteria
-
GET /v1/budget/limitsresponse includesenforced: boolean—trueiffisBudgetEnforced(c.env)returns true. -
PUT /v1/budget/limitsresponse includes the sameenforcedfield (it already re-resolves and returns the full shape post-write, so the field goes in the same return object). -
store/openapi.yamlis updated:enforcedadded to the GET 200 response schema (alongsidestored,effective,consumption,remaining,maxOverride) and to the PUT 200 response schema. Description:"Whether these ceilings are currently enforced. When false the limits are stored and metered but never block a run (observe-only, see BUDGET_ENFORCE).". -
budget.test.tshas at least one test verifyingenforced: falsein the response whenBUDGET_ENFORCEis absent/off, and one verifyingenforced: truewhenBUDGET_ENFORCE: "1"— mirroring the enforcement-test pattern from the delegation-budget test suite. - A comment is added to
#475's tracking body (or its description updated) noting that the Budget UI must display the observe-only note whenenforced === false.
Technical notes
Pattern to follow — billing.ts GET /v1/billing/status:
routes/billing.ts:42:enforced: isPaywallEnforced(c.env)— one line, sourced from the lib helper.- Mirror:
isBudgetEnforced(c.env)is already exported fromlib/delegation-budget-store.ts:274. Import it intoroutes/budget.tsalongside the existingresolveAccountCeilingsimport (budget.ts:19–22).
Response shape delta (GET and PUT):
{
"stored": { ... },
"effective": { ... },
"consumption": { ... },
"remaining": { ... },
"maxOverride": { ... },
"enforced": false // ← add this
}Files to touch:
workers/api/src/routes/budget.ts— bothGETandPUThandler return objects (lines ~118–146 and ~243–266).workers/api/src/routes/budget.test.ts— add enforcement-state assertions.store/openapi.yaml— lines ~8084–8117 (GET schema) and ~8150 (PUT schemaadditionalProperties:trueshould be replaced with an explicit object withenforced).
Related issues:
- [decision] Demote AI budget circuit breakers to observe-only (BUDGET_ENFORCE, default off) — keep structural caps + meter; re-enable at paid launch #485 — parent decision (observe-only switch,
isBudgetEnforced). - [frontend] Budget & limits settings surface — show 24h consumption vs each ceiling + distance-to-limit, and edit the limits (console owner + admin) #475 — Budget & limits settings UI (parked; must render
enforced:falsenote when resumed). - [mcp] get/set AI budget limits via MCP — account ceilings, per-tree defaults, Loop iteration cap #476 — get/set budget limits via MCP (parked; MCP tool response should include
enforcedtoo, as a follow-on to this ticket). - [enhancement] Per-run knobs configurable too — per-tree budget ($5/50/depth-4) and Loop max-iterations, via the same UI + MCP surfaces #477 — per-run knobs (parked).
- [epic] AI budget & circuit-breaker limits are user-configurable via UI + MCP (today they are hardcoded constants) #478 — epic.
- [bug] PUT /v1/instances/:id/behaviour answers 200 with rejected: [] for a body it ignored entirely — an unwrapped patch is a silent no-op #463 — silent no-op precedent (PUT accepting a body that changes nothing visible).
Priority: Medium. Route is reachable and harmless, but the honesty gap grows if #475 ships on top of it without the enforced flag — a user would see a polished settings panel managing limits that do nothing.
Activity