DEV Community

BrantLockwood468
BrantLockwood468

Posted on

Spend Limit Enforcement Autonomous Agents Cannot Edit Preserves Event Attribution

Autonomous agents need a spend limit they cannot edit, enforced by the billing service rather than the worker. The deciding constraint is attribution: when a developer-tools platform queues events during a backend outage, every accepted charge must still belong to the right account after replay.

TL;DR: Put the limit, current reservation total, and signing authority outside the agent's process. Let the agent request a reservation before paid work, attach that reservation to every queued event, and let the backend settle it exactly once. The agent may choose actions inside its allowance. It must not be able to rewrite the allowance itself.

The before-and-after mental model

Before: an agent reads limit = 5000, performs work, and increments a local counter. That counter is merely a suggestion. The same process that decides to spend can edit its configuration, retry a timed-out call, discard local state, or emit an event under the wrong account. During an outage, the backend cannot immediately contradict it.

After: a trusted control plane owns the policy. It issues a short-lived, signed reservation bound to an account, operation, amount, and unique event ID. The execution worker can present that capability, but it cannot mint a larger one. The event collector stores the identifier with the durable event. Settlement is idempotent, so replay changes delivery timing, not the billed total.

Here is the diagram in words: policy store -> reservation service -> untrusted worker -> durable event log -> billing ledger. The arrow back from the ledger closes or releases the reservation. There is no arrow from the worker to the policy store.

This framing treats budget enforcement as a security boundary and an accounting invariant. Availability still matters, but it does not get permission to blur account ownership. No attributable reservation means no billable execution.

A copyable reservation boundary

Here is a compact TypeScript shape. It omits transport and signature implementation on purpose; those belong in trusted infrastructure, not in an example that might imply home-grown cryptography is safe.

type AccountId = string;
type EventId = string;

type SpendRequest = Readonly<{
  accountId: AccountId;
  eventId: EventId;
  units: number;
  operation: "index" | "generate" | "evaluate";
}>;

type Reservation = Readonly<{
  accountId: AccountId;
  eventId: EventId;
  units: number;
  expiresAt: string;
  token: string;
}>;

interface BudgetAuthority {
  reserve(request: SpendRequest): Promise<Reservation>;
}

interface DurableEventSink {
  append(event: Readonly<{
    accountId: AccountId;
    eventId: EventId;
    units: number;
    reservationToken: string;
    occurredAt: string;
  }>): Promise<void>;
}

async function runBillableAction(
  authority: BudgetAuthority,
  sink: DurableEventSink,
  request: SpendRequest,
  perform: () => Promise<void>,
): Promise<void> {
  if (!Number.isSafeInteger(request.units) || request.units <= 0) {
    throw new Error("units must be a positive safe integer");
  }

  const reservation = await authority.reserve(request);
  await perform();
  await sink.append({
    accountId: reservation.accountId,
    eventId: reservation.eventId,
    units: reservation.units,
    reservationToken: reservation.token,
    occurredAt: new Date().toISOString(),
  });
}
Enter fullscreen mode Exit fullscreen mode

The worker never accepts an account ID or unit count after reservation. It copies both from the authority's response. That small choice blocks a nasty attribution mistake: reserving against account A, then emitting the completed event under account B.

Use one eventId across reservation, execution, queueing, and settlement. Make the ledger reject a second settlement for that identifier. Define units as integers rather than floating-point currency, and validate them at every trust boundary. These are design rules, not claims about a particular API.

The order exposes a trade-off. Reserving before execution can strand capacity when execution fails. Reserving after execution can permit work that the budget would have rejected. For autonomous workloads, reserve first, then give reservations an expiry and an explicit release path. The ledger remains authoritative.

Why do autonomous agents need a spend limit they cannot edit?

Because a control is not a control when the controlled process holds its write credential. Prompt instructions such as "never exceed this number" can guide planning, but they cannot enforce an invariant against tool calls, duplicated workers, stale state, or modified code.

Retries happen.

Keep policy-writing credentials out of the worker environment. OWASP's Secrets Management Cheat Sheet recommends least privilege, narrow authorization, rotation, expiration, and auditable secret use. Applied here, the agent gets only the capability needed to request or consume a bounded reservation. Administrative budget changes use a separate identity and an audited path.

Fail closed for new paid work when the authority cannot confirm capacity. Meanwhile, already-reserved work can continue within its signed scope, and completed events can wait in the durable queue. This is a deliberate availability trade-off. It limits the outage blast radius without throwing away events that already passed authorization.

Do not log reservation tokens. Log the event ID, account ID, requested units, decision, policy version, and timestamps; then protect those logs from alteration and restrict access. The secret is evidence of authority. The audit record is evidence of what happened. Mixing them turns observability into a credential leak.

What happens when the backend is down?

The event path and the policy path need distinct states. A worker may have an unexpired reservation while the billing ledger is unavailable. In that case, append the completed event to durable storage with its original account and event identifiers. Do not invent a replacement account, silently expand the reservation, or count the retry as fresh spend. On recovery, replay events in any delivery order while settlement deduplicates by eventId. Expired unused reservations are released according to policy; completed events carrying valid reservations are settled once. Keep the raw event immutable and record corrections as new ledger entries, so an attribution repair leaves an audit trail. A useful alert is semantic, not merely infrastructural: page on reservation denials rising unexpectedly, settlement lag approaching reservation expiry, duplicate settlement attempts, or account mismatches. Track queue depth and oldest-event age as metrics. Correlate all of them by policy version and account, while keeping credentials out of labels. Test the transitions: start with an approved reservation, interrupt the ledger, enqueue the event, replay it twice, and assert one settlement. Then try a mismatched account, a changed unit count, an expired token, and a worker identity attempting a policy write. Each should produce a stable denial and an audit event. The trade-off is explicit: preserve attribution accuracy even when that means pausing new paid work.

Short version: the worker owns intent; the authority owns permission; the ledger owns truth. Keep those responsibilities separate, especially while the network is unhealthy.

No exceptions.

The operational decision rule

A sound design can answer four questions from durable records: who authorized the spend, which account received it, what bounded amount was approved, and whether that event has already settled. If any answer depends on mutable worker memory, the boundary is too weak.

Choose reservation duration from measured queue and execution latency, then alert before the margin disappears. Longer validity helps work survive outages but enlarges the window in which stolen capabilities remain useful. Shorter validity reduces that window but can reject legitimate delayed events. Write this trade-off into policy and review it using observed latency distributions rather than intuition.

Keep budget changes boring: authenticated, authorized, separately logged, and unavailable to the execution identity. That leaves the autonomous agent free to plan quickly while the account platform preserves exact billing attribution.

References

Top comments (0)