Skip to main content
In sixty seconds you will write a policy, protect a refund function, run one refund autonomously, have a second one wait for a human, approve it from the shell, watch a mutated call refused, and read the receipts. Every block on this page runs offline against a fake remote, and the outputs shown are from a real run.
1

Write the policy

In an empty directory, save this as ctrlrun.yaml. Amounts are integer minor units: cents, not euros. Both ends of every band are bound, because an upper bound alone lets a negative amount through, and a refund of a negative amount is a charge.
runnable
Anything not listed here is denied. There is no default-allow.
2

Protect the function

Save this as agent.py. The decorator names the action and the effect key; the context names who is acting. stripe here is a stand-in that records calls instead of making them.
runnable file=agent.py
Run it once with python agent.py:
3

Approve it from the shell

The request id is what ctrlrun approve takes. The approval is bound to the hash of the exact action a human would see: stripe.refund, txn_2, €2,000, refund-agent.
runnable
The grant names the hash it authorizes and when it lapses. Ids and hashes are generated per run; yours differ.
4

Present the approval, and try to abuse it

Save this as approved.py. The first call presents the approval for the action it was granted for and runs. The second presents the same approval for a different amount, which matches nothing: the approval was bound to €2,000 and has already been spent.
runnable file=approved.py
Run it with python approved.py:
One call reached the fake remote in this process, the approved €2,000. The €5,000 never did.
5

Read the receipts

runnable
Three receipts: the €100 refund that ran on its own, the €2,000 refund that ran on the approval, and the €5,000 attempt, blocked.Every executed action has one: who, what, the decision, the approval it used, the effect key, the outcome, and the hash of the policy that decided it. ctrlrun inspect <action_id> shows one action’s whole history. Both are in .ctrlrun/receipts.jsonl and .ctrlrun/events.jsonl as one JSON object per line.

What you just saw

  • Per-action policy. €100 ran, €2,000 waited, €5,000 would have been denied outright.
  • Approval binding. The approval matched the exact action it was granted for and nothing else.
  • Effect keys. refund:txn_2 was reserved when the approved call ran; a second worker presenting the same key would have been refused.
  • Receipts. Everything above is in the evidence log, in order.
What you did not see is a lost reply. That is the case CTRLRun exists for, and Outcomes and AMBIGUOUS is where to read it next.

If it didn’t work

  • denied: no principal is available: the call ran outside ctrlrun.context(...). Every protected call needs a principal, and a missing one is denied.
  • PolicyError: ... could not be read: there is no ctrlrun.yaml in the working directory. ctrlrun init writes a starter.
  • ActionDenied ... unknown_action: the action name in the decorator does not match a key under actions:. Unknown actions are denied.

Next