Dev.to Security πŸ” Cybersecurity πŸ‘ 0 πŸ“– 5 min read

Consent Covers What an Agent Touches, Not What It Spends

In the first post I argued that an agent should start every session with nothing and borrow access from its user. In the second, that access should be checked on every call, not granted per tool. Both posts are about wh

In the first post I argued that an agent should start every session with nothing and borrow access from its user. In the second, that access should be checked on every call, not granted per tool.

Both posts are about what an agent can touch. Readers in the comments pointed at what they leave out. heyvaldemar put it in one line: consent covers the tools, not the spend. Kevin Hall gave the answer: spend fits the same rule as access.

This post is about that.

Every call was allowed. The bill wasn't.

Picture an agent asked to summarize a ticket backlog overnight. Every call it makes is inside its grants: read tickets, call the model, write a summary. Somewhere around ticket 400 it starts retrying a malformed response, and each retry re-sends the whole context. Nothing it does is forbidden. In the morning, the access log is clean and the model bill is not.

Or picture a prompt injection that doesn't try to steal anything. It just asks the agent to "be thorough" and re-read every attachment ten times. OWASP calls this unbounded consumption and lists denial of wallet as one of its harms. Every permission check passes, because nothing is being accessed that shouldn't be.

Access control answers "may it?" Spending needs a different question: "how much?"

Today the budget belongs to the agent

In most setups, the model gateway sees one credential: the agent's. Quotas and token budgets are keyed on that, so every user of the agent draws from the same pool. One person's runaway session can spend the allowance for everyone, and nobody can say afterwards whose session spent what.

That's the same mistake as the service account in the first post, applied to money. The agent holds a standing budget that no user agreed to and that isn't bound to any user.

A budget is a grant

So treat spend like everything else the agent borrows. The user lends a budget for the session, the same way they lend access:

Run this with up to 200,000 tokens.

And it follows the same rules:

Access grant Budget grant
Covers What the agent can touch How much it can spend
Lent by The user, per request The user, per session
Never more than What the user has right now What the user, or their team, has left
Capped by The template's ceiling The template's per-session limit
Subagents get A subset of the parent's access A slice of the parent's remaining budget
Ends With the session With the session, and unspent budget doesn't carry over

The subagent row is the one that matters most. A parent with 50,000 tokens left can't start five subagents with 50,000 each. It hands out slices, and the slices add up to what it has. "Inherit down, never up" works for money too.

Enforce it where the money is spent

A line in the system prompt asking the agent to be economical is not a limit. As Kevin put it, a promise in the prompt is not an enforcement point. The cap has to live where the spending happens: the model gateway, and any tool that costs money per call.

That runs into a practical problem heyvaldemar described in a follow-up: the model gateway usually never sees the person, only the agent. The least invasive fix I can see is to mint the agent's credential for the model gateway per session, with the gateway as its only audience and the session id inside. The gateway keys the budget on the session, and IAM answers which person the session belongs to. That doesn't add a second token to every call.

Make the ceiling hard

There's a catch that makes most token budgets soft. You only know how many tokens a request used after the response comes back, so the request that crosses the limit is served, and only the next one is refused. "Up to N" quietly becomes "N plus one request".

You can close that gap:

  1. Count the input before the call. You know what you're about to send.
  2. Cap the output to what's left. Set the request's maximum output to the remaining budget minus the input.
  3. Reserve, then settle. Hold the worst case before the call and charge the actual usage after it, like a card pre-authorization. That keeps parallel subagents from all spending the same remainder at once.

As a sketch, not a library:

def call_model(session, request):
    budget = iam.budget_for(session)          # what's left of this session's grant
    input_tokens = count_tokens(request)      # known before the call
    max_out = min(request.max_tokens, budget.remaining - input_tokens)
    if max_out <= 0:
        raise BudgetExhausted(session)        # stop, and ask the user for more

    hold = budget.reserve(input_tokens + max_out)
    try:
        response = model.create(request, max_tokens=max_out)
    except Exception:
        hold.release()
        raise
    hold.settle(response.usage)               # charge what was used, free the rest
    return response

One caveat: this only holds if your count matches what the provider bills. Tool definitions, cached prompts and reasoning tokens all count somewhere, and your counter has to agree with the invoice.

When the budget runs out, ask

When a session hits its limit, the agent stops and asks, the same way it asks for access:

Finishing the last 40 tickets needs about 60,000 more tokens. Allow?

The prompt shows the amount, not just "allow more spending?", for the same reason the second post argued for showing the actual call. People can judge a number. Bypass mode shouldn't approve more budget on its own, and an organization should be able to cap how much a user can top up.

What you get

  • A runaway loop stops at a known cost, not at the end of the month.
  • A prompt injection can't drain the shared pool, because there isn't one. Each session spends its own grant.
  • Spend has a name on it. Every token belongs to a session, and every session belongs to a person, which makes chargeback and audit simple.

Access says what an agent may touch. A budget says how much it may spend doing it. Both are things the user lends, and neither should outlive the session.

πŸ“° Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.