pip install ctrlrun, Python 3.11 or later, and an empty directory. If you
would rather see it before you type it, Try it in your browser runs the five demo
scenarios with no install.
1
Write the policy
In an empty directory, save this as Anything not listed here is denied. There is no default-allow.
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
2
Protect the function
Save this as Run it once with
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
python agent.py:3
Approve it from the shell
The request id is what The grant names the hash it authorizes and when it lapses. Ids and hashes are generated per
run; yours differ.
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
4
Present the approval, and try to abuse it
Save this as Run it with One call reached the fake remote in this process, the approved €2,000. The €5,000 never did.
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
python approved.py:5
Read the receipts
runnable
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_2was 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.
If it didn’t work
denied: no principal is available: the call ran outsidectrlrun.context(...). Every protected call needs a principal, and a missing one is denied.PolicyError: ... could not be read: there is noctrlrun.yamlin the working directory.ctrlrun initwrites a starter.ActionDenied ... unknown_action: the action name in the decorator does not match a key underactions:. Unknown actions are denied.
Next
- Cookbook: the same shape for a deploy, an IAM grant, a deletion, an email, an MCP server.
- Three ways in: decorator, gateway, adapter.
- Effect keys: what the
effect=template names, and why it is not a request id. - Why.