Documentation
¶
Overview ¶
Package limits enforces the stack.yaml `limits:` block: dollar budget caps with calendar-aligned windows and token-bucket rate limits, both scoped to one client, server, or tool. It implements the gateway's CallGate seam for pre-call checks and CostSettler for post-call spend settlement, and owns a small durable ledger so budget spend survives daemon restarts.
Enforcement is check-then-settle: a call is admitted against spend already recorded, and its own cost lands after it completes. Concurrent or in-flight calls can therefore overshoot a cap by their own cost; the next call after the cap is reached is denied. Budgets govern attributed cost only — a call whose model cannot be priced settles nothing.
Index ¶
- func DefaultBurst(callsPerMinute int) int
- type BudgetStatus
- type EntryStatus
- type Period
- type Policy
- func (p *Policy) CarryOver(old *Policy)
- func (p *Policy) Flush(_ context.Context)
- func (p *Policy) Gates() []mcp.CallGate
- func (p *Policy) SettleToolCallCost(ctx context.Context, call mcp.GateCall, costUSD float64)
- func (p *Policy) Start(ctx context.Context)
- func (p *Policy) Status() StatusReport
- func (p *Policy) Stop()
- type RateStatus
- type StatusReport
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func DefaultBurst ¶
DefaultBurst returns the bucket capacity for a rate limit that does not set one: a few seconds of the sustained rate, never below five.
Types ¶
type BudgetStatus ¶
type BudgetStatus struct {
MaxUSD float64 `json:"max_usd"`
SpentUSD float64 `json:"spent_usd"`
Percent float64 `json:"percent"`
Period string `json:"period"`
WarnAtPercent int `json:"warn_at_percent,omitempty"`
WindowStart time.Time `json:"window_start"`
WindowEnd time.Time `json:"window_end"`
}
BudgetStatus is one budget's consumption within its current window. All numeric fields are always present: a zero spent_usd is a real zero, not an unknown (the UI's em-dash convention is for absent data only).
type EntryStatus ¶
type EntryStatus struct {
// Kind is "budget" or "rate".
Kind string `json:"kind"`
// Scope is "client", "server", or "tool"; Key is the configured value.
Scope string `json:"scope"`
Key string `json:"key"`
// State is "ok", "warn" (budget past its warn threshold), or "exceeded".
State string `json:"state"`
Budget *BudgetStatus `json:"budget,omitempty"`
Rate *RateStatus `json:"rate,omitempty"`
}
EntryStatus is one limit's snapshot, shared by GET /api/limits and `gridctl limits`. Exactly one of Budget or Rate is set, matching Kind.
type Period ¶
type Period string
Period is a budget's calendar reset cadence.
const ( PeriodDaily Period = "daily" PeriodWeekly Period = "weekly" PeriodMonthly Period = "monthly" )
Budget reset periods. Windows are calendar-aligned in the supplied time's location (in production, the daemon's local timezone): daily resets at midnight, weekly on Monday 00:00, monthly on the 1st at 00:00. This is a deliberate divergence from cloud gateways' UTC alignment: a single operator's "five dollars a day" means their day.
type Policy ¶
type Policy struct {
// contains filtered or unexported fields
}
Policy is the compiled, enforcement-ready form of a config.LimitsConfig. A nil *Policy means no limits block was configured; every method is nil-safe and permissive. Entries are immutable after compile; only the per-entry window state mutates, under per-entry locks.
func NewPolicy ¶
NewPolicy compiles the limits block. A nil or empty config returns a nil policy (no limits, byte-identical legacy behavior). When budgets exist and ledgerPath is non-empty, prior spend is loaded from the ledger: entries whose stored window matches the current one resume, stale windows reset. A corrupt or missing ledger logs a WARN and starts fresh; it never fails.
func (*Policy) CarryOver ¶
CarryOver adopts state from a retiring policy so a hot reload never resets enforcement: budget windows carry for entries whose scope, key, and period are unchanged (cap changes deliberately keep the counter, and spend merges by maximum so a settlement that raced the swap is never lost), and rate limiters are reused for entries whose scope, key, rate, and burst are unchanged (an unrelated stack edit must not refill a drained bucket). It then marks the old policy retired, forwarding any late settlements here.
func (*Policy) Flush ¶
Flush writes the ledger atomically (temp file + rename). Failures WARN and are retried on the next dirty signal; spend is never worth crashing over.
func (*Policy) Gates ¶
Gates returns the policy's pre-call gates in canonical order: rate limits before budgets, so a rate-limited caller gets the cheaper check's message. A nil policy returns nil.
func (*Policy) SettleToolCallCost ¶
SettleToolCallCost implements mcp.CostSettler: it adds the call's priced cost to every matching budget window. Runs synchronously on the dispatch path, so it is a few short critical sections and a channel nudge; the ledger write happens on the flusher goroutine.
func (*Policy) Start ¶
Start launches the debounced ledger flusher. It is a no-op for a nil policy, a policy with no budgets, or an empty ledger path, so a stack without budgets gains zero goroutines. Idempotent: only the first call starts the goroutine. ctx cancellation and Stop both terminate the flusher after a final flush.
func (*Policy) Status ¶
func (p *Policy) Status() StatusReport
Status snapshots every configured limit. Budget windows are rolled first so a report requested after midnight never shows yesterday's spend. A nil policy reports Configured: false with an empty (non-nil) entry list.
type RateStatus ¶
RateStatus is one rate limit's configuration snapshot.
type StatusReport ¶
type StatusReport struct {
Configured bool `json:"configured"`
Entries []EntryStatus `json:"entries"`
}
StatusReport is the full limits status payload.