# `budget` — `BudgetTracker` _(no submodules)_ ## Purpose This module owns the per-request resource-budget enforcement for the sandboxed tier. Sampling primitives (CPU percentage, resident memory, wall-clock progress) are supplied by the worker host because they live outside this package's dependency budget; this module aggregates samples against the policy's `budgets` block and raises `SandboxPermissionError(code: SAND_PERM.BUDGET_EXCEEDED)` on the first newly-breached dimension. The tracker is idempotent across repeat samples on the SAME dimension: once a dimension has breached, subsequent ticks against that dimension do NOT re-emit the deny counter or re-throw — the dimension is considered "tripped" until the tracker is discarded at request end. ## Features - `BudgetTracker.tick(sample)` — consumes one `{ cpuPercent, memoryMb }` sample plus the wall-clock from the injected `Clock`. - `BudgetTracker.elapsedMs()` — milliseconds elapsed since construction. - Idempotent breaches — `cpuPercent`, `memoryMb`, `wallClockMs` each emit at most once per tracker instance. - Throws `SandboxPermissionError` with `code === SAND_PERM.BUDGET_EXCEEDED` on the first newly-breached dimension and increments `plugin.budget_exceeded_total{plugin, budget}`. ## Usage: Basic ```ts import { BudgetTracker } from "@suaveplan/api-sandbox-permissions/budget"; import { createPermissionInstruments } from "@suaveplan/api-sandbox-permissions/instruments"; import { PermissionPolicy } from "@suaveplan/api-sandbox-permissions/policy"; const policy = new PermissionPolicy({ permissions: { budgets: { cpuPercent: 50, memoryMb: 256, wallClockMs: 30_000 } }, }); const instruments = createPermissionInstruments({ plugin: "billing" }); const clock = { now: () => Date.now() }; const tracker = new BudgetTracker({ policy, instruments, clock }); tracker.tick({ cpuPercent: 10, memoryMb: 64 }); // within budget — no throw ``` ## Usage: Advanced ```ts import { BudgetTracker } from "@suaveplan/api-sandbox-permissions/budget"; import { createPermissionInstruments } from "@suaveplan/api-sandbox-permissions/instruments"; import { PermissionPolicy } from "@suaveplan/api-sandbox-permissions/policy"; import { SAND_PERM_CODES, SandboxPermissionError } from "@suaveplan/api-sandbox-permissions/error-codes"; const policy = new PermissionPolicy({ permissions: { budgets: { wallClockMs: 30_000 } }, }); const instruments = createPermissionInstruments({ plugin: "billing" }); // Injectable Clock — drive the wall-clock breach deterministically. let t = 0; const clock = { now: () => t }; const tracker = new BudgetTracker({ policy, instruments, clock }); t = 30_001; try { tracker.tick({ cpuPercent: 0, memoryMb: 0 }); } catch (err) { if (err instanceof SandboxPermissionError) { console.assert(err.code === SAND_PERM_CODES.BUDGET_EXCEEDED); // err.context = { capability: "budget", reason: "wallClockMs" } } } // Subsequent ticks against the same dimension are no-ops — the // dimension is already tripped. t = 35_000; tracker.tick({ cpuPercent: 0, memoryMb: 0 }); // returns silently ``` ## API Reference - `interface BudgetSample { cpuPercent: number; memoryMb: number; }` - `interface BudgetTrackerOptions { policy; instruments; clock: Clock; }` - `type BudgetBreach = "cpuPercent" | "memoryMb" | "wallClockMs"` - `class BudgetTracker` - `constructor(options)` - `tick(sample): void` — throws on first newly-breached dimension. - `elapsedMs(): number` - `readonly policy: PermissionPolicy` - `readonly instruments: PermissionInstruments` ## Implementation Notes The `Clock` is injected per `@suaveplan/types` — never pulled from `Date.now()` directly so test code can drive a deterministic timeline without `vi.useFakeTimers()`. The constructor records `startedAt = clock.now()` once; every subsequent `elapsedMs()` call subtracts from a fresh `clock.now()` so wall-clock progress is monotonic with the clock. `evaluate` is private and centralises the breach record so all three dimensions go through the SAME counter + throw codepath; this guarantees the conformance harness's "deny counter increments exactly once per breach" invariant holds for every dimension without per-dimension duplication.