
Deterministic authorization for AI agents
Everyone agrees that deterministic authorization should mean the same input gets the same answer. That definition is correct and it is half of the requirement. The half people skip is worse: the decision has to stay recomputable for as long as anyone might ask about it. A system that answers consistently but cannot explain itself a year later is deterministic in the way a wall is deterministic.
The four places an authorization path stops being deterministic
Only the first one is a model, and it is the one everybody argues about.
- The model. A plausible completion is not a decision. Nobody disagrees with this in a design review and everybody disagrees with it in a launch.
- The clock. A rule that reads the current time changes its answer at midnight, at a renewal window, at the moment a limit resets. That is not a bug, it is a policy, and it has to be written as one.
- Concurrency. Two requests that both read a remaining balance of one hundred euro and both allow a ninety euro purchase are deterministic individually and wrong together. The order of operations, not the arithmetic, is the problem.
- External state. A feature flag, a remote config, a third party call inside the decision path. The moment the answer depends on something outside the boundary, the boundary is not the boundary.
I lost the third one myself. My authorization endpoint checked the budget, wrote the decision, then recorded the idempotency key. Linear, obviously correct, and a race, because agents retry by nature and two identical requests arriving in the same millisecond both pass the check. An external audit found it. A bootcamp would not have.
The second property: a decision you can replay
Stability is about the next request. Recomputability is about the one you did six months ago. For an agent payment, that is the whole game, because six months ago is when the dispute arrives, and the person asking is not the person who built the engine.
Three things make a decision recomputable, and all three are cheap if you do them on day one and expensive if you do them later.
- Freeze the policy version in the record. A rule changed in April cannot explain a purchase from March. The record names the version of the boundary that was in force, not just the outcome.
- Freeze the inputs that produced it. The mandate, the amount, the merchant, the normalised request. If the record has to read live state to be understood, it will be understood wrong.
- Keep the hash chain. Each record carries the hash of the previous one, so a deletion or an edit is visible without trusting the database that stored it.
That third one sounds ceremonial until you meet it. A ledger that can be edited silently is a ledger that will be edited silently, and the person who edits it may not be malicious. They may be a migration, a retention policy, or a support engineer cleaning up a test row.
Idempotency has to be keyed on the intent
The other half of determinism is the retry. An agent that tries, fails to see a response, and tries again is behaving correctly. If your key is the request, you have two charges. If your key is the intention, you have one decision, one row, and one charge, and the retry returns the original answer.
100 concurrent requests, same intent, same idempotency key
ledger rows written ...... 1
charges created ........... 1
decisions returned ........ 100 identical
replay the recorded decision against the frozen policy
recomputed decision ....... identical
recomputed hash ........... identicalThis runs in the suite on every commit, which is the only reason I use the phrase verified by test about it. Everything else in this article is a design argument, and design arguments get the weaker label.
There is a version of this that sounds like process rather than engineering, and I want to be explicit about it, because it is the part that actually keeps the property. The test suite is the specification. If a behaviour is not asserted somewhere, it is not a behaviour, it is a hope, and it will be changed by a well meaning pull request six weeks from now. Every rule in my policy language has a test that says what it does at the boundary, because a rule engine without boundary tests is a rule engine with surprises.
The second half of the discipline is refusing new inputs to the decision path. It is always easier to add a lookup, a call, a flag. Every one of them makes the answer depend on something outside the boundary, and the boundary is what the record reconstructs months later. The rule I follow is that the decision function reads the request, the policy version, and the ledger, and nothing else. It is a constraint that feels arbitrary until the first incident, and then it is the only reason the incident can be explained.
What determinism does not buy you
A deterministic engine with a wrong policy is wrong faster and more consistently. Determinism does not make the boundary sensible, does not make the agent's choice good, and does not help you if the instruction the agent followed was the wrong instruction. It buys you one thing: when the dispute arrives, there is an answer that is the same answer as last time.
In a category where every product page ends with the same sentence about a better future, that is not a small thing. It is the difference between a claim you can check and one you have to believe.
I am building this at Noesia. The reasoning about where the model belongs is in why there should be no LLM in the authorization path, the category argument in the authorization layer for autonomous agents, and the record this all serves in why agent payments need an append-only audit trail. If you have a system where you can replay a decision from a year ago, I want to read how you did it, because I have not found another. The retry half of this is idempotent payments for AI agents.
Building Know Computer, a personal context layer for the AI era. Reply via [email protected].