Scenario 04 · adds asynchronous approval
Nobody's watching and it needs permission
The agent has been working for forty minutes. It's done nineteen things correctly and has arrived at the twentieth: a refund above the auto-approve limit. Its user went to bed at eleven. Failing the task is wrong. Proceeding is much worse. What it needs is to wait — and for waiting to be a thing the protocol has a name for.
Three wrong answers first
Fail, and make the human start over
The agent gives up and reports back. Forty minutes of context evaporates; in the morning the user re-runs it and hopes the same nineteen steps go the same way. This is what most systems do today, and it's the reason "human in the loop" has a reputation for meaning "the agent wasn't useful."
Pre-approve everything at the start
Grant the agent refund authority up front so it never has to ask. This is scenario 03's problem returning: the user approved a category months ago and now has no visibility into the act. It converts a moment of genuine oversight into a checkbox nobody remembers ticking.
Ask the user in the chat window
The agent prints "This refund is £900 — shall I proceed?" and waits for a reply. This feels like the right answer, and it is by far the most common design in agent frameworks. It has one fatal property:
The resource has no idea it happened. The refunds API sees a token with a refund scope and a valid request. Whether a human approved it, whether the agent asked at all, whether the "yes" came from the user or from text the agent read in a support ticket — none of that is visible to, or verifiable by, the system that has to enforce the decision. The approval lives entirely inside the least trustworthy component.
Approval has to be something the resource can demand and verify. Which means it has to happen somewhere the resource trusts, and come back as something the resource can check.
The shape that works
The resource challenges. A human answers. The token proves it.
OAuth already has this pattern for a weaker case. RFC
9470 lets a resource reject a request with insufficient_user_authentication and say what it
needs — "come back with a fresher session," "come back with MFA." The client goes and gets it, and returns
with a token the resource can verify. The resource sets the bar; the authorization server enforces it; the
client just relays.
// RFC 9470: the resource demands stronger authentication
→ 401 Unauthorized
WWW-Authenticate: Bearer error="insufficient_user_authentication",
error_description="Multi-factor authentication required",
acr_values="urn:mace:incommon:iap:silver",
max_age="300"
The OAuth Transaction Authorization Challenge draft takes that pattern and points it at the agent problem. Instead of "this user needs to authenticate harder," the resource says this specific operation needs approval, and hands back a signed challenge describing it.
The challenge is a JWT signed by the resource. That signature is what makes the whole thing work:
- The authorization server can verify the challenge is real — the agent didn't invent the operation being approved.
- The human is shown the resource's description of the act, not the agent's.
- The token that comes back refers to that challenge, so the resource can confirm the approval it gets is the approval it asked for.
The agent's role reduces to a courier. It carries a challenge it can't forge to a server it doesn't control, waits, and carries back a token it can't have influenced the contents of. That is exactly as much trust as an agent should be given.
The flow
Forty minutes in, at two in the morning
-
Agent→Refunds API
Requests a £900 refund with a perfectly valid token. The token is not the problem — the amount is.
-
Refunds API→Agent
401with a signed challenge describing the operation: this refund, this order, this amount, this customer. -
Agent→Authorization Server
Relays the challenge. It cannot alter it — the signature won't survive.
-
Authorization Server→Approver
Verifies the resource's signature, works out who is allowed to approve this (the user, their manager, whoever policy names), and pushes a prompt to them. This is CIBA's job, or a device-grant-style code, or an internal approvals queue.
-
Agent→Authorization Server
Polls. Receives
authorization_pending— for six hours, if that's how long the human sleeps. Nothing sensitive is held anywhere during the wait. -
Authorization Server→Agent
Approved at 07:14. The agent gets a token whose
authorization_detailsdescribe this refund and nothing else, valid for two minutes. -
Agent→Refunds API
Retries. The API checks that the token's details correspond to the challenge it issued, and processes the refund. The log records who approved it, when, and what they were shown.
// step 2 — the challenge
→ 401 Unauthorized
WWW-Authenticate: Bearer error="transaction_authorization_required",
challenge="eyJhbGciOiJFUzI1NiIsInR5cCI6ImNoYWxsZW5nZStqd3QifQ..."
// decoded payload — signed by the resource
{
"iss": "https://api.shop.example/refunds",
"aud": "https://auth.shop.example",
"jti": "chal_01K9M2X7",
"exp": 1787980000,
"authorization_details": [{
"type": "refund",
"order_id": "ORD-88213",
"amount": { "currency": "GBP", "amount": "900.00" },
"customer": "cus_4Q1ZR",
"reason": "damaged_on_arrival"
}]
}
// step 5 — waiting is a state, not an error
POST /token HTTP/1.1
grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atxn-challenge
&challenge=eyJhbGciOiJFUzI1NiIsInR5cCI6ImNoYWxsZW5nZStqd3QifQ...
→ 400 Bad Request
{ "error": "authorization_pending", "interval": 30 }
The transaction challenge draft is an individual submission from June 2026, not yet a working group document — parameter names will move. The pattern is the durable part, and it's the same one RFC 9470 and RFC 8628 already established.
Designing the interruption
The protocol is the easy half. The judgement calls:
- The resource decides, not the agent. If the agent chooses when to ask for approval, an attacker who controls the agent's input chooses when to ask for approval. Thresholds belong on the server side of the API.
- Approve operations, not sessions. A challenge that resolves into "the agent may now do refunds for an hour" has thrown away everything this rung bought. One challenge, one act.
- Make it idempotent. The agent may retry, crash and resume, or run twice. The challenge identifier should be the idempotency key so an approved refund can't become two refunds.
- Expire the challenge, generously. Minutes is too short for a sleeping human; forever is an approval sitting in someone's notifications for a task that finished last week. Hours, matched to how long the agent's work is still relevant.
- Show the approver the resource's words. The human is being asked to trust something. The one component whose description of the act they should not be reading is the agent's.
Everything below this point is about restricting what an agent can do. This is the first rung that makes an agent more capable: it can be trusted with longer, more consequential work precisely because there's a verifiable way for it to stop and ask. Guardrails that hold are what let you take your hands off.