Your AI agent asks for approval forty times a day.
The first few you read carefully. You check the recipient, you skim the draft, you think about it. Somewhere around the tenth you stop reading and start clicking, and by the afternoon the dialog has become a keystroke — a muscle movement between you and getting your actual work done. You are still technically in the loop. You are no longer a control.
We call this human-in-the-loop. It works more like a rubber stamp.
Removing the approvals is not the answer either. Both failures come from the same place: a check that looks at one action at a time cannot see the thing that actually hurts you, which is accumulation. Two hundred emails that were each individually fine. Fifty small refunds. A folder deleted one file at a time, every deletion defensible, the directory gone by Thursday.
Financial markets worked this out long before AI agents existed. A broker does not approve each of your trades. It watches one number — how much risk you are carrying against what you have put up to cover it. Cross the line and you get a margin call.
Stop gating actions. Start budgeting exposure.
Per-action approval fails at arithmetic, not willpower
Approval fatigue gets described as a human weakness. It is arithmetic. If a reviewer has twenty minutes of attention a day for agent oversight and the agent generates forty requests, each request gets thirty seconds whether it deserves thirty seconds or thirty minutes.
The deeper problem is what the dialog shows. It says “send this email.” It does not say “this is the thirty-eighth outbound message this run has produced, nine of them to external counterparties, none of them read back by anyone.”
Risk is not a property of an action. Deleting one file is a different act when it is the fourth deletion than when it is the four-hundredth. A control that evaluates actions independently is structurally blind to the only variable that matters, and no amount of reviewer discipline fixes a structural blindness.
Three components: ledger, gateway, policy bundle
The five steps only make sense once you know where the state lives, so here is the shape first.
An exposure ledger — a service with its own datastore, deployed separately from any agent. A tool gateway that every agent tool call passes through; we run it as an egress sidecar, though a central service behaves the same way. And a policy bundle holding the pricing rules, the margins per task type, and the hard limits.
The agent never talks to the ledger. The gateway does. The agent asks the gateway to send an email; the gateway identifies the caller, prices the action, reserves exposure against the run, and only then performs the side effect.
Workload identity carries more weight here than it first appears. Every agent process is bound to a SPIFFE identity, and the run’s lineage travels with that identity rather than inside the prompt. An agent cannot claim to be a different run, because the ledger derives the run from the SVID presented at the gateway — not from anything the model wrote.
Hot path is two operations: price and reserve. That needs to land in single-digit milliseconds, so the ledger keeps open exposure in memory behind an append-only write-ahead log. Everything else is asynchronous — evidence export to immutable storage, exposure aging, cap recalculation from track record, reconciliation against observed effects.
1. Price actions by exposure, not by count
Start crude. Read costs 0. A reversible write costs 1. An irreversible action costs 10. Irreversible and external costs 25. Multiply by how many things the action touches.
Pricing in Go:
type Class uint8const ( Read Class = iota ReversibleWrite Irreversible IrreversibleExternal)var base = map[Class]int{ Read: 0, ReversibleWrite: 1, Irreversible: 10, IrreversibleExternal: 25,}// Price returns the exposure an action puts on the books.// BlastRadius is the count of distinct entities the call affects:// recipients, rows, objects, repositories.func Price(a Action) int { return base[a.Class] * max(1, a.BlastRadius)}
The crudeness is deliberate. Teams stall for a quarter building a defensible risk model and ship nothing; four buckets and a multiplier will teach you more in two weeks of shadow mode.
The multiplier is what per-action gates miss. One email to one customer is 25. One email to four hundred customers is 10,000, and no margin you would reasonably set survives it. The agent cannot reach that action without stopping first — not because someone wrote a rule about bulk email, but because the arithmetic forbids it.
Classification is where the arguments happen. Our rule: reversible means your system can undo it without anyone else’s cooperation, inside the run’s lifetime, and you have tested the undo. A soft-deleted record with an exercised restore path is reversible. A message sitting in someone else’s inbox is not, however good your retraction feature claims to be.
2. Give every run a margin
A margin is a cap on open exposure for a task type. Not per agent and not global — per task type, because “triage inbound support tickets” and “reconcile a settlement break” deserve different numbers and always will.
With a margin of 100, an agent sends four customer emails on its own and the fifth waits.
The ledger entry as it goes on the books:
{ "entry_id": "exp_01HQ7F3A9K2M", "run_id": "run_7f3a1c", "root_run_id": "run_7f3a1c", "lineage": ["run_7f3a1c"], "principal": "spiffe://platform.internal/ns/agents/sa/refund-assistant", "task_type": "customer_refund_triage", "policy_digest": "sha256:9c1f...e07a", "action": { "tool": "email.send", "class": "irreversible_external", "blast_radius": 1, "target_digest": "sha256:4b2d...9911" }, "exposure": 25, "state": "reserved", "opened_at": "2026-09-14T15:04:05.221Z", "settled_at": null, "release": { "mode": "human_ack", "by": null, "at": null }, "evidence_uri": "s3://agent-evidence/run_7f3a1c/exp_01HQ7F3A9K2M.json"}
Two fields carry most of the value. policy_digest pins which version of the pricing and margin rules produced this number, so a year later you can say what the control was at the time instead of what it is now. target_digest proves which entities were touched without copying their contents into a risk store that has no business holding them.
The state machine is reserved → committed → released, with reserved → voided when the side effect never happened. You reserve before acting and settle after. Invert that order and a crash between the action and the bookkeeping hands the agent free exposure — the one accounting bug in this pattern that is genuinely dangerous rather than merely annoying.
A reservation that never settles must age into committed with an unknown outcome, never into voided. When you cannot tell whether something happened, carry the risk.
3. Release exposure when work is verified or undone
This is the step that changes agent behaviour and the one most implementations skip.
A reversible change that gets read back and confirmed frees its budget. An irreversible action stays on the books until a human acknowledges it. Nothing decays on a timer, ever.
Which makes verification part of the work instead of a virtue. An agent that writes forty records and never checks them is carrying forty units of exposure and will hit its margin. An agent that writes, reads back, confirms, runs almost indefinitely. You never told it to verify. You made verification the only way to keep going.
The release path in Rego, because this is policy rather than code:
package agents.margin.release# A reversible write clears once the gateway has observed a# read-back whose digest matches what was written.release[entry.entry_id] := "verified" if { entry := input.open[_] entry.action.class == "reversible_write" rb := input.readbacks[_] rb.entry_id == entry.entry_id rb.observed_digest == entry.action.target_digest}# Irreversible actions never self-clear.release[entry.entry_id] := "human_ack" if { entry := input.open[_] entry.release.mode == "human_ack" ack := input.acks[_] ack.entry_id == entry.entry_id ack.actor_kind == "human"}
The read-back digest has to come from the gateway’s own observation. An agent that clears its own exposure by reporting success has a budget it sets itself, which is not a budget.
4. The margin call interrupts before the side effect
When the next action would breach the cap, the run pauses before acting. The reservation trips the wire, which means the email has not been sent yet. Everything about the usefulness of this pattern depends on that ordering.
Then one summary goes to a human: what is open, what is next, how much more risk it is asking for.
MARGIN CALL — run_7f3a1c (customer_refund_triage)Margin: 100 Open exposure: 94 Requested: 25 Breach: 19Open positions 3 x email.send (external, unacknowledged) 75 19 x ledger.annotate (reversible, unverified) 19Next action email.send -> 1 recipient, draft digest 4b2d..9911 Reason: refund denial notice, case CS-44192Options: acknowledge open items / approve this action / close out
One interruption instead of forty. The human is reading a position rather than a request, and a position is something a person can actually reason about in thirty seconds.
Note what the summary does not include: a recommendation from the agent. We tried that and took it out. The summary describes the book; an agent arguing for its own next action reintroduces precisely the dynamic the pattern exists to break.
5. Close-out is code, not a runbook paragraph
No answer means stop and unwind what is reversible. Write that rule before the run, not during the incident.
Close-out is harder than it sounds, because unwinding half a batch can leave worse state than finishing it. So the policy is per task type and explicit about which direction is safer:
task_type: customer_refund_triagemargin: 100timeout: 20mclose_out: mode: unwind_reversible order: reverse_chronological on_partial_failure: freeze_and_page irreversible: leave_open_and_reporthard_limits: - tool: payment.transfer effect: deny # never priced, never available - tool: email.send max_blast_radius: 25 - tool: object.delete namespaces: ["prod-*"] effect: deny
Hard limits sit outside the margin system deliberately. They never consult the ledger and remaining budget does not buy them. For the Kubernetes-shaped ones we enforce with Kyverno at the cluster layer, so an agent with a spotless track record and a full margin still cannot delete a production namespace. Budget buys discretion, not permission.
Four invariants that keep the ledger honest
The ledger lives outside the model. A trader does not set their own margin requirement. If the agent can read its remaining budget, you have built a puzzle for it to optimise against rather than a limit.
Sub-agents draw on the parent’s margin. Spawning twenty workers must not mint twenty budgets. The lineage array exists for this: exposure rolls up to the root run and the gateway rejects any call whose SVID cannot produce an ancestry chain back to an open root. This is where the pattern leaks most often in production, and it leaks silently.
A clean track record raises the cap. Autonomy stops being an argument about trust and becomes a number you move on evidence. We recalculate caps weekly from settled outcomes.
Hard limits hold regardless. The first thing anyone asks for is an exception, which is why this is an invariant and not a guideline.
Where this pattern breaks
Price inflation. Nobody wants to be the engineer who classified the action that caused the incident, so everything drifts toward irreversible-external. Margins trip on action two, the team raises the margin until it stops tripping, and the cap becomes decoration. Watch the distribution of prices, not the breach count. If 80% of your actions cost 25, your taxonomy is dead.
The acknowledgement queue becomes the new rubber stamp. Irreversible exposure needs human acknowledgement, acks back up, agents stall, and someone bulk-clears four hundred items on a Friday afternoon. You have rebuilt approval fatigue one layer down. Treat ack latency as a platform SLO and alert on bulk acks that arrive with no evidence attached.
Decomposition gaming. An agent that cannot afford one irreversible write will find that many reversible writes, read back to clear the budget, then one cheap flip, gets the same result. Nobody teaches it this; it falls out of optimisation. Price cumulative effect over a window alongside per-call price, and watch for runs whose settled exposure sits far below their gross exposure.
Close-out causing the incident. Unwinding a partial batch is itself a change, executed under pressure, usually at a bad hour. Close-out ships with the task type and gets exercised in a game day or it does not exist.
Ledger availability. If the ledger is down, do you fail open or closed? Under Regulation SCI that is not really a choice — an unavailable control is an unavailable control, and you fail closed. Which puts the ledger in the critical path of the whole agent fleet, with the availability engineering that implies. People underestimate this one badly.
Shadow mode first, because you cannot stop the fleet
Four phases. The first changes nothing.
Shadow mode. Put the gateway in front of existing tool calls, price everything, record it, block none of it. Run two weeks. The number you want is peak open exposure per run per task type, and it is reliably higher than the team guesses. This phase also surfaces the taxonomy arguments while they are still free.
One task type, generous margin. Pick the task type with the clearest reversibility story. Set the margin at the 95th percentile of observed exposure so it trips roughly never. Keep every existing approval gate. You are testing plumbing, not policy.
Ratchet and release. Lower the margin until it trips a few times a week, and build read-back verification for that task type’s reversible actions. This is where behaviour changes, and where you start retiring the old per-action gates — one gate, one task type, with the exposure data as the argument.
Earn the caps. Caps move on track record only once settled-outcome data exists. Earned autonomy before you have outcome data is just a bigger margin wearing a story.
Auditors ask what the control did, not what it does
I work under SEC and CFTC oversight, and Regulation SCI shapes how we think about controls on systems that matter. The practical consequence catches teams late: a supervisor never asks what your control does. They ask what it did on 14 September at 15:04, and they expect an answer that does not depend on your current configuration.
A chat transcript where somebody typed “approved” is not that answer. A ledger entry is — policy digest that priced the action, identity that performed it, margin in force, lineage of the run, who acknowledged what and when.
This is why evidence export is a first-class part of the design rather than logging with ambitions. Entries land in immutable storage keyed by run, retained as long as the effects of that run matter.
A second benefit surprised me. Pricing forces a conversation between engineering and risk that otherwise never happens, because someone has to decide out loud whether an action is reversible. That conversation produces better controls than any volume of policy documentation.
Survivability is a lower bar than correctness, and a better one
The shift is one question wide. Stop asking “is this action allowed?” and start asking “how much uncovered risk is this agent carrying right now?”
Markets do not assume every trader is right. They assume someone will be wrong and make sure it is survivable. That is a weaker claim than correctness and a far more useful one, because it is achievable with systems we already know how to build — ledgers, limits, settlement, position reports.
Forty approval dialogs a day is not oversight. It is the appearance of oversight, and the appearance is worse than nothing, because it convinces everyone the question has been handled.
So: how does your team decide when a human gets pulled in today — every action, certain actions, or accumulated risk?
