The mechanism: one ordered check, two phases, a ledger that is never edited.
What onedoor checks, in which order, when the budget is spent, how a policy version comes into force, and what its records can and cannot show. Described for onedoor 0.8.1.
1 · The check order
Every action is one structured request, checked in a fixed order.
Scheduled, rule-fired, model-proposed or clicked by a person, every action is an ActionRequest: an action type, its parameters, its source and a rationale. One executor checks it against the policy table before anything touches the world. The first check that denies decides, with its own reason code.
default_deny and an out-of-range one bounds, and one that passes both is denied as kill_switch, not proposed. Bounds are checked before a person ever sees a proposal. A tier-3 action is proposed before dry-run and caps; dry-run is resolved before caps, so a rehearsal never spends a real budget.Default-deny
An action type no policy declares is refused as default_deny. Nothing is proposed, and nothing promotes itself.
Reversibility before autonomy
A tier 1 or 2 policy must name a compensating_command. Without one, the loader refuses to start, and an auto-tier action without an undo is demoted to a person's approval.
Effects share limits
Actions that declare the same effect (say money.egress) share one budget and one tier floor, however their tools are named.
2 · Two phases
Decide and reserve, act, then append what happened.
Caps are reserved inside the deciding transaction (BEGIN IMMEDIATE), so two concurrent requests cannot share the last slot. The action itself runs outside any database lock.
success and timeout settle it (a timeout is doubt, so onedoor assumes the action happened). failure settles it too, unless the report adds "no_effect": true. Only a positive statement that nothing happened releases money held: not_attempted, or failure with no_effect, each recorded as its own event. A calls-per-day count is never given back. The in-process executor puts a hard timeout on the action; behind the HTTP service, your enforcement point owns its own timeout.3 · Decision and enforcement
One decision point. Enforcement wherever your tools are called.
onedoor separates the classic authorization pair: a policy decision point, and enforcement points that ask it and then act. Any enforcement point, in any language, can ask the HTTP service. The same engine runs in-process behind the library and the integrations.
4 · Policy versions
A policy set is a version, named by its digest, changed only by ratifying.
The Studio keeps drafts in their own store. Nothing in a draft touches the rules in force. A draft becomes the version in force only through ratification: a page that shows the new version's digest, what it replaces and every changed rule in sentences, and asks for a name.
The loader's own words
Every draft is checked by the engine's own loader, stage by stage. What it would refuse at start-up is listed apart from how a loadable rule will behave at decision time; the two are never merged.
A receipt per ratification
Each ratification seals a receipt: the version, the one it replaced, the time, the name given, and the kill switch's state. The Verify page re-checks it, with the receipt and the policy snapshot downloadable.
No un-ratify
To go back, you ratify again: a new version and a new receipt. Nothing decided under an earlier version is changed. The name given is what the store knows, not an authenticated identity.

5 · The ledger
Append-only rows, and checks that say what they rest on.
Every decision, result, denial, dry-run and kill-switch block is a row with a typed reason code. Database triggers forbid updates and deletes. The export writes one JSON object per row with a .sha256 file beside it.
| State | Meaning |
|---|---|
verified | Checked against something the store does not hold: a trusted key id passed in from outside, or a published anchor root. |
self_consistent | The signature matches a key in the same store, or a proof checks against a root the store itself carries. Real information, and not independence: whoever can write the store supplies both halves. |
absent | Not produced for this row, for example rows newer than the last anchor, or signing not switched on. Shown as absent, not as a fault. |
The receipt page (python -m onedoor.viewer) is static and read-only. Every value on it is read from a verified artifact; if the evidence does not check out, it shows the failure state and none of the receipt's values.
6 · With onetrace
A decision reference, joinable to the export by digest.
When a deployment declares its name (ONEDOOR_ISSUER), each decision that permits, denies or proposes carries a small reference (dry-runs and tier-0 reads carry none): format onedoor-decision-ref/1, the request id, a digest of the decision's ledger row, the verdict (permit, deny or propose) and the issuer. Without a declared name, no reference is returned: never a guessed one.
onetrace-verify --decisions checks that gated steps cite a decision, that a step that acted cites a permit, and that each reference matches a row of the export by digest. Either product's records stand on their own without the other.7 · The limits
What the mechanism does not cover.
Parameters, not intent
Rules match parameters. A command assembled indirectly carries nothing to match; a URL pattern matches text unless you opt into the URL block, which matches the canonical target. Use an egress control that fails closed for anything that matters.
Who times the action
The in-process executor puts a hard timeout on the act. Behind the HTTP service, onedoor never sees the act itself: your enforcement point owns its timeout, and reports timeout when it cannot tell what happened.
The documented deployment
One process, one SQLite writer behind an internal lock. Several writers on one store, without that, can see a window between checking a cap and consuming it.
One obligation
onedoor implements one obligation, present_bound: the permit may be used only at the audience it names. In 0.8.1 the in-process executor and the MCP proxy refuse to act on any permit that carries it, since neither presents to an audience; the LangChain middleware does not check it.
8 · Coming next
The permit in every caller's hands, and the recipient's signed reply.
Part of this is in 0.8.1 already; no dates for the rest. The receiving system checks a signed permit on its own, without calling onedoor, and next signs back what it did:
- Bound to the request (in 0.8.1). When a policy asks for one, onedoor issues a permit naming the exact action by digest, the key that must sign the request, a short lifetime and a one-time id; the request must carry its body's digest. The library returns it; the decision service does not hand it to its caller yet. Next: every surface does.
- Checked in a fixed order by the recipient (in 0.8.1).
onedoor.permitholds the recipient's check: thirteen steps, stopping at the first failure, with the permit used up only at the last one, so a refused request never burns it. - A signed confirmation back (next; not built yet). The recipient signs which permit and which request it acted on, and whether the effect happened, was refused or is unknown. A onetrace record can then show an action authorized and its effect confirmed, and check both.
Back to the overview, or start on the Developers page.