When an agent has no autonomous authority for a payment, how does a human "yes" become a safe, once-only execution?
Most spending policies include a human line: amounts above $X, new recipients, or flagged actions need a person's approval. But what does approval mean in an agent system? If "approved" just means the model was told "okay, go ahead," then the human has only added one more layer of conversation — not a control.
The useful property of approval is that it converts a human decision into bounded, executable authority: authority over this specific request, for this specific amount and recipient, usable exactly once.
| Naive pattern | What actually happens |
|---|---|
| "Ask the model to wait for a yes" | The model is the thing being attacked. A yes in chat is a fact in context, not a control. |
| "Enable the payment tool after approval" | That's permanent capability — the agent now spends freely until someone remembers to disable it. Approval became a switch, not a decision. |
| "Log the approval message" | Logs describe. They don't stop a second execution of the same intent. |
Approval that is not bound to the specific intent, and not consumed by execution, drifts back into the category of things an agent can argue with or replay.
1. payment request (agent, amount, recipient, purpose)
│
2. policy evaluation
│
3. APPROVAL REQUIRED ← over the autonomous line / new recipient / flagged
│
4. human approves ← sees the actual request: amount, recipient, agent
│
5. signed one-time grant issued (bound to amount + recipient + agent + policy version)
│
6. executor verifies grant
│
7. grant consumed
│
8. execute once
│
replay of the same grant → refused (step 6 fails)
Each property matters:
res = shield.authorize(agent="trading-agent", amount=250, to="exchange.example.com")
if res.decision == "APPROVAL":
# human reviews the actual request…
res = shield.approve(res.approval_id, by="ops-reviewer")
# re-evaluated against current policy; only ALLOW proceeds
if res.decision == "ALLOW":
grant = issuer.issue(agent="trading-agent", amount=250,
currency="USD", merchant="exchange.example.com",
policy_version=res.policy_version)
ok, reason = executor.verify(grant, agent="trading-agent", amount=250,
currency="USD", merchant="exchange.example.com")
# ok → execute; second verify of the same grant → (False, "REUSED")
| Request | Outcome |
|---|---|
| $25 → trusted merchant (under autonomous line) | ALLOW — no human needed |
| $250 → trusted merchant (over line) | APPROVAL → human grants → executes once |
| same grant replayed | REFUSED — consumed already |
| human approves, then merchant gets blocked before execution | DENY — approval re-checked against current policy |
SpendShield implements this workflow: approval
requests carry an approval_id, approve() re-evaluates against the live policy, and ALLOW
produces the signed one-time grant that the executor consumes. Open source, MIT, Python + MCP.
Try it interactively in the policy playground — raise the approval line and watch
a payment switch from ALLOW to APPROVAL.
pip install spendshielduvx --from spendshield spendshield-mcp --policy policy.yaml — authorize_payment → spend_approve