onedoor optional · standalone · open source

Your agent can act. Decide what it may do, before it does.

onedoor is a policy engine for agent actions. Every action an agent proposes is checked against a policy you wrote, before anything touches the world: permitted within limits, held for a person, or refused with a reason. Every decision on a well-formed request is written to an append-only ledger.

Free and open source (Apache-2.0) · runs on your machine · no account · pip install onedoor

Illustration: an agent proposes three actions. onedoor permits a 40 euro transfer within its day cap, denies wire.anywhere with default_deny, as no policy declares it, and holds payouts.schedule as proposed until a person approves. Every decision goes to an append-only ledger.
IllustrationThe screens further down are the real product.

The problem

Most guardrails govern what a model says. Agents do things.

An agent with tools can move money, change records, send messages. The question that matters before each one is plain: is this action allowed, within what limits, and who said so?

Usually Rules in prompts and code

  • What the agent may do is scattered across prompts, tool code and good intentions.
  • An action nobody thought about is allowed: nothing said no.
  • Limits like "no more than €500 a day" are counted by hand, if at all.
  • Afterwards, nobody can show which rule allowed what.

With onedoor One door, one policy

  • Every action goes through one check, against a policy file you can read.
  • An action no policy declares is refused, with a reason that says what would allow it.
  • Caps are reserved race-free, so two requests cannot share the last euro.
  • Every decision is written with its reason code, and never edited afterwards.

What you get

Positions most frameworks leave as wishes.

Default-deny

An unlisted action type is not an error and not a pass. It is refused as default_deny, and the refusal says how to allow it.

Bounds before a person

Parameters are checked against declared ranges first, so an approval screen only ever shows a request that is physically sane.

Caps, counted honestly

Per-action and per-effect budgets (€ per day, per month, calls per day), reserved inside the deciding transaction.

Undo before autonomy

An action runs without asking only if its policy names how to reverse it. The loader refuses to start otherwise.

A kill switch that wins

Read before any policy lookup, and it outranks approvals already granted. While it holds, nothing runs on its own and nothing new is proposed. Reads stay open.

An append-only ledger

Decisions, results, denials, dry-runs and kill-switch blocks, each with a typed reason code. Exportable, and checkable row by row.

Four tiers

How much an action may do on its own.

Each policy puts an action type in one tier. New action types can start in dry-run: decided and recorded as "would have executed", with nothing done and no budget spent.

TierMeaningFrom the shipped payments examples
0 · observeRecorded, never blocked by the kill switch. For reads.ledger.read
1 · autoRuns without asking: reversible and within bounds.payments.reverse, declared at tier 1; its money.egress effect lifts it to an effective tier 2
2 · auto, cappedRuns without asking, up to cumulative caps.payments.transfer: €500/day, €5000/month, €0.01–€2000 each
3 · confirmProposed, and waits for a person to approve before it runs.payouts.schedule

Product tour

The Policy Studio, on the shipped payments examples.

Real screensScreenshots of onedoor 0.8.1's own Studio and receipt page, run on this site's machine with the payments example policies and a handful of sample requests. The Studio binds to your own machine only: nothing it shows leaves it.

01 · Policies

What is allowed, and under what limits

Every policy in force, with its tier, caps, bounds and effects. The page says plainly what it is not: a list of what is blocked. Nothing needs to be listed to be blocked.

The Studio's Policies page. Header: in force 0c12ff2d…5bab, never ratified, 6 policies, 2 effects, loopback only, nothing leaves this machine. A table of six actions: ledger.read review OBSERVE; payments.reverse allowed AUTO; payments.transfer allowed AUTO_CAPPED, EUR 500/day, EUR 5000/month, amount_eur 0.01 to 2000, effect money.egress; payouts.schedule review CONFIRM; refunds.issue allowed AUTO_CAPPED EUR 300/day; webhooks.post allowed AUTO_CAPPED. Each marked covered.
02 · Drafts

Upload a file; see what the loader would refuse

A policy file becomes a draft, even one the engine would refuse at start-up. The draft shows each refusal from the engine's own loader, in its own words: here, two auto-running actions that name no way to undo them.

A draft uploaded from a file, bad.yaml, with two rules: crm.update_contact at AUTO and invoices.pay at AUTO_CAPPED. Changes: this draft cannot be previewed, it would be refused at load. Validation lists two problems: each is an auto-executing tier with no compensating_command, so it cannot auto-execute.
03 · The editor

A guided form and the raw rule, never out of step

Both panes are drawn from one parsed rule, so they cannot disagree. Validation runs on the engine's own loader as you type. The rules in force are not touched by anything on this page. (Its note that a numeric bound refuses a decimal-string amount is out of date: 0.8.1's engine accepts them.)

Editing refunds.issue inside the draft raise the refund ceiling. Left, the guided pane: action type, tier AUTO_CAPPED (2), dry run off, compensating command payments.reverse, cost parameter amount_eur, cap EUR per day 400 and per month 2000. Right, the rule as JSON with bounds, caps and the compensating command.
04 · Review the change

What changes, in sentences, before you ratify

Each changed rule is described as it was and as it would become. Ratifying is a deliberate step with a name on it, and it seals a receipt for the new version. There is no un-ratify: going back is a new version, and nothing already decided is rewritten.

The draft raise the refund ceiling. Changes: refunds.issue modified. Was: capped at EUR 300/day, EUR 2000/month. Would become: capped at EUR 400/day, EUR 2000/month, otherwise the same bounds, effect money.egress, undoable by payments.reverse within 900 seconds. Validation: the validator found no problems.
05 · Ratified

A new version in force, and its receipt

The version in force is a digest of the policy set. The receipt records what it replaced, when, under which name, and the kill switch's state at that moment. Each ratification can be checked again later on the Verify page.

Ratified. In force: 0d38f505061f47a2e22a34246e4506e812957f0e3219fbbeaf98d146a52b0e86. Replaced 0c12ff2d…5bab, receipt 38cbe77c…ac52, ratified by session operator on duty, kill switch not engaged.
06 · Live state

The kill switch, every budget, every pending approval

How much of each action's cap is consumed, reserved and free; open reservations; approvals and when they expire. The Studio only shows the kill switch. Throwing it lives on the decision service, behind its own admin key.

One thing this screen gets wrong: the shipped policies cap the money.egress effect at €750 a day and €6000 a month, and 0.8.1's Studio shows that effect's limit as "none declared". The engine enforces the cap; the display is a known defect, reported to onedoor.

The Live state page. Kill switch: not engaged, with a note that it outranks everything including granted approvals. Budgets: effect money.egress consumed 165, its limit shown as none declared (a display defect: the policies cap it at 750 a day and 6000 a month); payments.transfer EUR per day consumed 120 of 500; per month 120 of 5000; refunds.issue 45 of 300 per day and 45 of 2000 per month, each with a bar.
07 · History

Every decision, by action, verdict and version

Filter the ledger by action, verdict, policy version, origin or date. The Studio's note on this page that no caller identity is recorded is out of date: 0.8.1's decision service writes a keyed fingerprint of the decide key to the rows it permits. Rows from in-process callers carry none.

The History page with filters for action, verdict, policy version, request origin and dates. Five decisions: payments.transfer denied budget_exhausted; ledger.read executed observe; payouts.schedule proposed tier_confirm; payments.transfer denied bounds; wire.anywhere denied default_deny. Each under policy version 0c12ff2d…5bab, origin llm.
08 · The decision receipt

One decision, and the checks that back it

A static, read-only page built from the ledger: the verdict, the frozen parameters, the budget at the moment of decision and the policy version. Every value on it is read from a verified artifact; if the evidence does not check out, it shows the failure and none of the values.

Try it on your machine

The onedoor decision receipt page: payments.transfer, denied. DENY budget_exhausted: a value cap was reached for this effect; the reservation was refused and nothing executed. Request id, source llm, frozen params amount_eur 400.00 to acct-2, tier nominal 2 effective 2, budget at decision: payments.transfer.eur_day, limit 500, consumed 120, remaining 380, and the policy version digest.

Optional, and better together

onedoor decides. onetrace records. Each works without the other.

onedoor is a separate product with its own package. Use it on its own to govern what agents may do. Add it beside onetrace and each gated step of a run cites the decision that allowed it, which anyone can check against onedoor's export.

onedoor alone

Policies, approvals, caps and the kill switch, with an append-only ledger and its own receipt page. No onetrace needed.

onetrace alone

A checkable record of each run and the comparison that finds the first difference. No policy engine needed.

Together

A step marked as gated must cite a onedoor decision. The verifier fails a step that acted on a deny, or cited nothing, and matches each reference to a row of onedoor's export.

From a real run of onetrace 0.6.0 against onedoor 0.8.1's decision service, checked with onetrace-verify --decisions:

[PASS   ] decisions: gated steps cite a decision -- 1 gated step(s), each citing a decision
[PASS   ] decisions: references are well formed -- 1 reference(s), each a well-formed onedoor-decision-ref/1
[PASS   ] decisions: a step that acted cites a permitting decision -- 1 acting step(s), each citing a permitting decision
[NOT-RUN] decisions: the permit covers the recorded action -- no permit recorded
[PASS   ] decisions: the decision is in the export -- 1 reference(s), each matching a row in the export by digest

The NOT-RUN row is honest: onedoor 0.8.1's decision service does not hand a signed permit to the caller yet (see Coming next). The code for this run is on the Developers page.

Where it plugs in

In front of the tools you already have.

An HTTP decision service

Any enforcement point, in any language, asks POST /v1/decide, acts, then reports the outcome. Admin keys are separate from decide keys: the process that asks is not the process that grants.

A Python library

Decide-and-reserve, then report the result, around your own call. The same checks, in-process.

An MCP proxy

Point an MCP host at the proxy instead of the tool server. Every tools/call goes through the full check; unknown tools are denied by default.

LangChain middleware

Wraps tool calls in a LangChain or LangGraph agent, including a mode that pauses for a person's approval.

An AI gateway example

An experimental LiteLLM guardrail adapter in onedoor's repository (not in the package; the litellm extra installs what it needs): model allow-lists, daily caps, and the gateway's MCP tool calls.

Signals out

Approval requests to a webhook (Slack-compatible payload), and OpenTelemetry spans and decision counters with the otel extra.

Honest by design

What it doesn't do, stated up front.

onedoor's own documentation lists its known limits. These are the ones to read before you trust a boundary to it.

Parameter rules can be dodged

A command built indirectly (for example, decoded at run time) carries nothing a parameter rule can match. URL patterns match the string, not where it leads, unless you use the opt-in URL block. Put a fail-closed network control in front of anything that matters.

Not its own witness

A signature checked against a key found in the same store reads self_consistent, never verified. Independence needs a key or an anchor the store does not hold.

One process, one writer

The documented deployment is one process with one SQLite writer. Running several writers on one store without that serialization opens a window between check and act.

Operator keys guard the service

Admin keys separate approving from asking on the HTTP decision service. The library and the MCP proxy's operator paths run in your own process, so who may approve there depends on how you deploy them.

Coming next

From "allowed" to "done, and confirmed".

What 0.8.1 does not do yet. Built in the open, described here so you know where it is going; no dates.

  • Signed permits on every surface. 0.8.1 can already issue a short-lived signed permit, bound to the exact action, the sender's key and the request body and usable once, and its library includes the receiving system's own check. The decision service does not hand that permit to its caller yet; next, every surface will.
  • The recipient's signed confirmation. The system that acted signs what it did with which permit, so a record can show the action was authorized and its effect confirmed.
  • Decision references on every surface. Today the decision service and the library return them, and the MCP proxy builds one without passing it on; next, the MCP proxy, the gateway adapter and the LangChain middleware return them too.