Per-Job Cost/Token Budget
Per-job budget ceiling caps the count of
calls in a job — every call is worth exactly 1, regardless of what it
actually does. That’s blind to a job that stays well under any request-count
cap while burning real money: ten calls to a cheap read_file tool and ten
calls to an expensive run_llm_completion tool trip the same counter by the
same amount, even though one job is orders of magnitude more expensive than
the other. Per-job cost/token budget assigns each tool a declared cost and
caps the total of those costs per job — the same (tenant, identity, session) job concept, a different unit.
Enable with:
features:
job_cost_budget: true
job_cost_budget:
ceiling: 1000 # optional -- defaults to 1000 when unset
tool_costs: # optional -- per-tool declared cost
run_llm_completion: 50
read_file: 1
default_cost: 1 # optional -- cost for a tool not in tool_costs; defaults to 1
session_window_seconds: 300 # optional -- fallback session width when no session header is sent; defaults to 300
Sessions
A “job” is the same (tenant, identity, session) triple taint
tracking established and per-job budget
ceiling reuses — the explicit
X-Wardline-Session header when the agent framework stamps one per run, or
a per-identity sliding TTL window as a fallback when it doesn’t. See Taint
tracking → Sessions for the exact
mechanics — cost budget uses the same fallback logic, with its own
job_cost_budget.session_window_seconds (defaults to 300, independent of
taint.session_window_seconds/job_budget.session_window_seconds — cost
budget works without either of those features being on). Cost budget’s
counters live in their own store, never shared with job budget’s — the two
features never read or write the same key, even though both key on the
identical triple.
Declared cost, not measured usage
Each gated tool call has a declared cost, looked up from tool_costs by
tool name; a tool absent from tool_costs falls back to default_cost
(itself defaulting to 1 when unset). An explicit 0 for a declared tool is
honored as a genuinely free call, not treated as “unset”. This is a static
table an operator writes, not a measurement of what a call actually did —
see the boundary section below.
Two consumption points
A hard proxy gate, always on when the flag is on. Every gated call adds its declared cost to the job’s running total; a call that would push the total past
ceiling(strictly greater than, not equal) gets429and is not forwarded upstream. The audit entry records decisioncost_budget_exceeded— a distinct value from boththrottled(budget enforcement’s rate-limit decision) andjob_budget_exceeded(per-job request-count), so all three enforcement dimensions stay individually greppable in the audit trail rather than collapsing into one signal.An optional
input.cost_over_budgetfield in Rego, mirroringinput.job_over_budget. A hard 429 is the simplest outcome; pairingcost_over_budgetwithapprovalinstead routes the over-budget job through approval workflow — an operator sees it and decides whether to let it continue, rather than a flat block:package wardline.authz default allow = false approval { input.cost_over_budget } allow { input.identity == "agent-abc123" }See the Rego reference for the full result contract.
input.cost_over_budgetis read-only from a policy’s perspective: consulting it never adds to the job’s total — only the hard gate above does that, exactly once per request, so a policy that checks the field on every evaluation can’t itself trip the ceiling.
Grant-override
An approved retry is admitted even over the cost ceiling. Cost budget
shares its grant-carve-out with the per-job request-count gate: when
approval_workflow is on and an operator approves a held call, the
single-use grant that admits the retry overrides every ceiling dimension the
needs_approval routing could have tripped — cost budget, job budget, or
both at once — not just whichever policy field happened to trigger the hold.
The operator approved the call, not one specific objection to it. Outside
that narrow, one-call carve-out, every other caller still hits the hard
cost_budget_exceeded deny. The job’s running total still accumulates the
grant-admitted call’s cost, so the ceiling stays accurate for every
subsequent call on that job — only this one call is let through, not the
ceiling itself.
Boundary — what this is and isn’t
- A static per-tool cost table, not measured usage.
tool_costsis a number an operator writes ahead of time per tool name. Nothing in this feature parses a response body, a token-usage field, or any other upstream-reported signal — the declared cost is charged whether the call actually used that much or not. If a tool’s real cost varies per call (e.g. a completion whose token count depends on the prompt), the declared number is a flat estimate, not a per-call measurement. - No real-money billing. The ceiling and the per-tool costs are whatever unit the operator chooses (tokens, an arbitrary weight, dollars as a mental model) — there’s no currency conversion, no invoice, no connection to a billing system. It’s an internal accounting number for enforcement, not a financial record.
- No cross-job or cross-identity aggregation. Same as per-job budget
ceiling: the ceiling is scoped to one job — one
(tenant, identity, session). Ten sessions each just underceilingis nowhere near any limit this feature enforces; it isn’t a fleet-wide or per-identity total. - No manual reset. There’s no reset endpoint or admin action that clears a job’s running total mid-session. The ceiling holds until the session itself rotates — the header value changes, or (in fallback mode) the TTL window lapses — the same boundary that ends taint’s session and per-job budget’s.
- The dashboard’s cost-budget view lists jobs by opaque key and total, not decomposed tenant/identity/session — the same one-way, length-prefixed key format per-job budget ceiling uses. Correlate a key to a specific identity via the audit log instead, where all three fields are already recorded per entry.
When features.postgres_storage is also on, the per-job cost counter is
backed by a shared Postgres table instead of an in-process map, the same
pattern per-job budget ceiling uses for its
own Postgres option — every replica pointed at the same database enforces
the same ceiling, so it no longer scales with replica count. A
Postgres-backed check that errors or times out fails open (an allowed
call, marked with a FailedOpen audit reason), the same
availability-over-enforcement posture as budget enforcement, per-job budget
ceiling, and credential revocation.