← All Writing

Per-Call Authorization Cannot Bound an Agent Run

An allowlist answers one question: is this single call permitted. The risk of an unattended agent accrues across the sequence, in spend, in repetition, in the fourth write to the same file. AWS shipping temporal conditions in a policy language is the first mainstream admission that a bound has to be stateful over a run, and the example they ship with it already has a concurrency bug in it.

A 3D wireframe diagram drawn in thin glowing white lines on a solid black background, with no other colour anywhere. Across the lower right of the frame, a single row of eight identical small wireframe cubes recedes into the distance from lower left to upper right, evenly spaced along one shared line, and each cube sits inside its own separate thin upright square ring, so there are eight cubes and eight rings, one ring per cube, every ring the same size and every cube passing cleanly through the ring around it. Above and behind that row, a large flat horizontal wireframe grid plane fills the upper middle of the frame, drawn as a regular square lattice of thin white lines seen in shallow perspective and stretching almost the full width of the image. A single vertical column of seven of the same small cubes, stacked directly on top of one another, rises through that plane at its centre: the top three cubes stand clear above the surface, the fourth intersects the grid exactly where the column crosses it, and the remaining three hang below. Wide empty black space fills the left side and the corners of the frame.

Every agent harness has a permission prompt, and the prompt asks a question with a clean answer: may this process run rm, may this agent call the payments API, may this tool read that file. The question is well formed, the answer is checkable, and a policy engine can produce it in under a millisecond.

Here are two failures that question cannot see. Both are AWS’s own examples, published alongside sequence-aware policy in Bedrock AgentCore:

An agent looks up a customer’s account, then transfers money to a different account number, because each call was judged on its own.

An agent places a series of orders that each sits under the approval threshold, because nothing is tracking the total against the budget.

And the sentence that follows them:

Every one of those requests was legitimate. The problem appears only in the pattern.

My claim is that an allowlist bounds a call, and a run is not a call. The risk of an unattended agent accrues across the sequence: in spend, in repetition, in the fourth write to the same file, in the read that was fine until it followed a different read. A stateless policy layer is a filter, and plenty of teams have been calling theirs a limit.

I should say up front that I have the same gap. The agents I run unattended have their permissions evaluated one call at a time, and the only thing standing between a bad sequence and my credit card is that the box cannot reach very much. That is a boundary argument, not a policy argument, and I come back to the difference at the end.

The industry has been making a single decision finer#

AWS built Cedar to work that way on purpose, and says so in the Dogwood announcement:

Cedar supports efficient point-in-time authorization decisions where each request is evaluated in isolation, with no dependency on past actions. As a result, it can draw a safety envelope around any single action, but it was not designed for expressing rules about sequences of actions.

Everything shipping this year sharpens that single decision. Google’s successor to BeyondCorp pushes authorization down to individual actions and resources, on the argument that the old assumptions, that accessors are human and that actions happen at human speed, are “no longer sufficient”. Cloudflare added optional OAuth scopes so a user can decline part of what an application asks for, because “an MCP server might request a broad set of permissions because in theory an agent could use all of them, while most users would not want an agent to have that much access”. Both are real improvements to the resolution of a single decision, made once, about one action, and neither can express the sentence “this run has done enough”.

The money version is the easiest one to check#

Copilot Code Review reached Azure Repos billed per review, with tokens converted into credits at one credit per cent, and review cost varying with effort, pull request size and custom instructions. The control you would reach for is a budget. From the documentation:

Budgets only notify you. They don’t stop reviews or change any resources.

Charges then appear in Azure Cost Management 48 hours after a review completes. Put those two facts together and an automatic review policy on a busy repo runs for two days before its cost is visible anywhere, and the mechanism you configured to bound it sends email. Nothing on that path can stop the sequence, because nothing on it counts the sequence while it happens.

Agent payment rails have the same shape with better marketing. Cloudflare’s virtual wallets give an account holder three limits: an allowance, an allowlist of approved merchants, and a maximum transaction size. Two of those bound one payment. The allowance is the right shape, a running total, but it belongs to the wallet rather than to the run, so expressing “this task may spend twenty dollars” means minting a wallet per task. That works, and it is provisioning standing in for policy, which you will feel the first time a task has to be resumed rather than restarted.

What a rule over a sequence actually looks like#

Dogwood is AWS’s attempt to put this in a language instead of in everyone’s application code. It embeds Cedar, keeps existing policies working, and adds exactly one thing: a second kind of clause, when temporal, “whose condition can also look at what came before the request”. Those clauses evaluate over traces of events, where an event is a tool call request or its outcome. Four operators cover the common cases, named in the announcement as questions:

  • Did this happen? formerly
  • How many? count_within
  • How many different? count_distinct_within
  • How much in total? sum_within

Here is the approval example from the announcement:

permit ( principal, action == AgentCore::Action::"SellShares", resource )
when temporal {
formerly within 1h AgentCore::Action::"ApproveSale"::response{
input.stock: context.input.stock,
input.shares: context.input.shares,
output.approved: true
}
};

Read that as an operator rather than as a policy author. A sale is permitted when an approval happened, for the same stock, for the same number of shares, within the last hour, and that approval returned true. One authorization decision now requires four joins against a history that has to exist, be queryable at call latency, and be trustworthy. The set membership test it replaces required none of that.

There is a bug in the middle of the example, on purpose#

The part of the announcement I keep thinking about is a policy AWS ships knowing it is wrong, to make a point about the state model. It caps transfers by summing amounts from response events:

Because this policy only sums the amounts associated with responses, an agent can circumvent the intended limit by issuing many concurrent transfer requests before any one of them resolves.

Sum the responses and the limit fails under concurrency. Sum the requests and you charge the run for transfers that errored, so a flaky downstream service eats the budget. There is no free answer here, only a choice about which way to be wrong, and the choice matters more for this workload than for any other, because an agent decides its own parallelism. A service issues the requests its callers send it. An agent issues ten at once because it inferred that ten would be faster.

Where this loses#

A smaller box beats a smarter policy for most of the harm. Docker’s argument for dropping the prompt entirely is the strongest objection to everything above, and it is mostly right: “Guardrails only work when something outside the agent enforces them”, so give the agent “a boundary it can’t cross and you get the speed with almost none of the exposure”. Per-call prompts also fail on their own terms, since “each approval forces you to context switch and breaks the flow that made the agent worth using”. An ephemeral sandbox holding credentials scoped to one task bounds the sequence without any policy state at all, because an agent cannot overspend an API it cannot reach. Sequence-aware policy earns its keep only for actions that are individually legitimate, individually approved, and irreversible: money moving, mail leaving, data crossing a boundary. For everything else, buy a smaller box first.

The event log lands in the hot path of every tool call. AWS is unusually candid about the bill. Temporal conditions “do not currently support the powerful automated reasoning analysis tools that Cedar provides”, so a policy set that uses them can no longer be formally analyzed, which was one of the better reasons to adopt Cedar. Evaluation needs stateful tracking, and “the time complexity of evaluation can depend on the length of the event log”. AWS says the reference interpreter is “for exploring and testing the language, not for running authorization in production”. Around it you need trusted timestamps, authenticated events, durable storage for traces, and a retention policy for logs that now hold whatever your agents touched.

That adds up to a durable, trusted store consulted on every tool call, at call latency. It is a new dependency, and it arrives with a question nobody enjoys answering. When the policy store is unreachable, does the agent fail open or fail closed? Fail open and the bound was decoration. Fail closed and a single store outage halts the fleet mid-run, killing exactly the long-lived stateful things you built the platform to protect.

You may already have the cheap version. A hard cap on steps, tokens or wall clock per run is stateful policy with one variable, and most harnesses ship one. It cannot express “no external send after reading that document”, but it does bound the run, which is more than an allowlist manages. If that cap is not set in your harness today, set it this week and leave the policy engine shopping for later.

The takeaway#

Write down three rules for your agents that use the word “after” or the word “total”. No external send after touching customer data. No more than fifty dollars of tool spend in one run. No second write to the same file without a fresh read in between. Now look at your permission layer and check whether it can express any of them.

If the answer is that you could get there with a wrapper somebody would have to write, that wrapper is a policy engine plus a durable event log, and it is worth building deliberately rather than at two in the morning during the incident that made you want it.

Then answer the two questions the rules depend on. What happens to a run when the thing holding its history is down. And, before that one, what object in your system is the run at all. Dogwood evaluates against a session’s events and the AgentCore tally applies within a session, so if you cannot name the record that represents one agent run and point at the store that holds its history, there is nothing for sequence-aware policy to attach to. That record is the first thing to build, and most platforms I have seen do not have it yet.

Deciding where a bound lives, what it costs on the request path, and what happens to it during an outage is platform work rather than model work, and agents have made it urgent. If that is the sort of problem your team is hiring for, here is what I do.