Skip to content

[enhancement] /v1/budget/limits sets ceilings that don't gate anything — surface the observe-only (BUDGET_ENFORCE off) state in GET/PUT #486

Closed

Description

@serge-ivo

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:

Out of scope:


Acceptance criteria

  • GET /v1/budget/limits response includes enforced: boolean — true iff isBudgetEnforced(c.env) returns true.
  • PUT /v1/budget/limits response includes the same enforced field (it already re-resolves and returns the full shape post-write, so the field goes in the same return object).
  • store/openapi.yaml is updated: enforced added to the GET 200 response schema (alongside stored, 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.ts has at least one test verifying enforced: false in the response when BUDGET_ENFORCE is absent/off, and one verifying enforced: true when BUDGET_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 when enforced === 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 from lib/delegation-budget-store.ts:274. Import it into routes/budget.ts alongside the existing resolveAccountCeilings import (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 — both GET and PUT handler 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 schema additionalProperties:true should be replaced with an explicit object with enforced).

Related issues:

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendBackend / Worker / API workenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions