onedoor 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. onedoor-0.3.0/LICENSE +202 -0
  2. onedoor-0.3.0/PKG-INFO +201 -0
  3. onedoor-0.3.0/README.md +176 -0
  4. onedoor-0.3.0/onedoor/__init__.py +1 -0
  5. onedoor-0.3.0/onedoor/config.py +19 -0
  6. onedoor-0.3.0/onedoor/connectors/__init__.py +6 -0
  7. onedoor-0.3.0/onedoor/connectors/errors.py +11 -0
  8. onedoor-0.3.0/onedoor/connectors/mock.py +46 -0
  9. onedoor-0.3.0/onedoor/guardrail/__init__.py +56 -0
  10. onedoor-0.3.0/onedoor/guardrail/approvals.py +98 -0
  11. onedoor-0.3.0/onedoor/guardrail/audit.py +130 -0
  12. onedoor-0.3.0/onedoor/guardrail/bounds.py +49 -0
  13. onedoor-0.3.0/onedoor/guardrail/caps.py +103 -0
  14. onedoor-0.3.0/onedoor/guardrail/decision.py +312 -0
  15. onedoor-0.3.0/onedoor/guardrail/errors.py +35 -0
  16. onedoor-0.3.0/onedoor/guardrail/executor.py +207 -0
  17. onedoor-0.3.0/onedoor/guardrail/killswitch.py +34 -0
  18. onedoor-0.3.0/onedoor/guardrail/models.py +169 -0
  19. onedoor-0.3.0/onedoor/guardrail/policy.py +51 -0
  20. onedoor-0.3.0/onedoor/guardrail/policy_loader.py +73 -0
  21. onedoor-0.3.0/onedoor/guardrail/registry.py +29 -0
  22. onedoor-0.3.0/onedoor/guardrail/undo.py +79 -0
  23. onedoor-0.3.0/onedoor/mcp/__init__.py +1 -0
  24. onedoor-0.3.0/onedoor/mcp/demo_server.py +100 -0
  25. onedoor-0.3.0/onedoor/mcp/proxy.py +235 -0
  26. onedoor-0.3.0/onedoor/service/__init__.py +1 -0
  27. onedoor-0.3.0/onedoor/service/app.py +270 -0
  28. onedoor-0.3.0/onedoor/service/notify.py +70 -0
  29. onedoor-0.3.0/onedoor/service/telemetry.py +48 -0
  30. onedoor-0.3.0/onedoor/store/__init__.py +1 -0
  31. onedoor-0.3.0/onedoor/store/bus.py +44 -0
  32. onedoor-0.3.0/onedoor/store/clock.py +28 -0
  33. onedoor-0.3.0/onedoor/store/db.py +93 -0
  34. onedoor-0.3.0/onedoor.egg-info/PKG-INFO +201 -0
  35. onedoor-0.3.0/onedoor.egg-info/SOURCES.txt +38 -0
  36. onedoor-0.3.0/onedoor.egg-info/dependency_links.txt +1 -0
  37. onedoor-0.3.0/onedoor.egg-info/requires.txt +17 -0
  38. onedoor-0.3.0/onedoor.egg-info/top_level.txt +1 -0
  39. onedoor-0.3.0/pyproject.toml +31 -0
  40. onedoor-0.3.0/setup.cfg +4 -0
onedoor-0.3.0/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
onedoor-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,201 @@
1
+ Metadata-Version: 2.4
2
+ Name: onedoor
3
+ Version: 0.3.0
4
+ Summary: A tiered guardrail/policy engine for agentic systems: default-deny, bounds, caps, approvals, dry-run, kill switch, and reversibility as a precondition for autonomy.
5
+ Author: Shamik Saha
6
+ License: Apache-2.0
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: pydantic>=2.10
11
+ Requires-Dist: pydantic-settings>=2.7
12
+ Requires-Dist: pyyaml>=6.0
13
+ Provides-Extra: service
14
+ Requires-Dist: fastapi>=0.115; extra == "service"
15
+ Requires-Dist: uvicorn>=0.34; extra == "service"
16
+ Provides-Extra: otel
17
+ Requires-Dist: opentelemetry-api>=1.27; extra == "otel"
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=8.3; extra == "dev"
20
+ Requires-Dist: mypy>=1.14; extra == "dev"
21
+ Requires-Dist: ruff>=0.8; extra == "dev"
22
+ Requires-Dist: fastapi>=0.115; extra == "dev"
23
+ Requires-Dist: httpx>=0.27; extra == "dev"
24
+ Dynamic: license-file
25
+
26
+ # onedoor
27
+
28
+ **A tiered guardrail engine for agentic systems.**
29
+ The model proposes; the policy layer disposes.
30
+
31
+ Every action in an agentic system — scheduled, rule-fired, LLM-proposed, or
32
+ human-clicked — is a structured `ActionRequest` evaluated by one executor
33
+ against a policy table before anything touches the world. There is one door.
34
+ Nothing else is allowed to call a connector.
35
+
36
+ ```
37
+ kill switch → policy lookup / default-deny → tier-1 integrity (no undo, no
38
+ autonomy) → bounds → dry-run → caps → two-phase execute → append-only audit
39
+ ```
40
+
41
+ ## Why another guardrail project?
42
+
43
+ Most "guardrails" govern what a model may *say*. This engine governs what an
44
+ agent may *do* — and it takes positions most frameworks leave as wishes:
45
+
46
+ - **Default-deny.** An unlisted action type is not an error and not a pass:
47
+ it resolves to propose-and-confirm, with the reason recorded.
48
+ - **Reversibility is a precondition for autonomy.** An auto-tier action whose
49
+ policy declares no compensating command is demoted to human approval at
50
+ runtime — and the policy loader refuses to boot if a Tier-1 entry lacks one.
51
+ Undo is not a feature; it is the admission ticket to auto-execution.
52
+ - **The kill switch outranks everything, including prior consent.** Checked
53
+ before policy lookup; an already-approved action arriving while the switch
54
+ is engaged is blocked (without spawning an approval loop). Reads stay exempt
55
+ — you want visibility *during* the incident.
56
+ - **Bounds are validated before a human ever sees a proposal**, so the
57
+ approval screen can only contain physically sane requests. The human decides
58
+ *whether*, never has to catch *whether it's insane*.
59
+ - **Rehearsal must not spend a real budget.** Dry-run is resolved before cap
60
+ accounting; new action types start in dry-run and log "would have executed".
61
+ - **Caps are reserved race-free** inside the deciding transaction
62
+ (`BEGIN IMMEDIATE`), so two concurrent requests cannot share the last slot.
63
+ - **Two-phase execution.** Tx A decides, reserves caps, and records intent;
64
+ the connector call runs outside any DB lock under a hard timeout; Tx B
65
+ appends the result. A hung smart-plug API cannot hold the engine hostage,
66
+ and a crash leaves an honest "intended, unconfirmed" trail.
67
+ - **The audit log is append-only** — decisions, results, denials, dry-runs,
68
+ and kill-switch blocks, all with typed reason codes, never updated in place.
69
+ - **Policies are data, not code** (`config/policies.yaml`): tiers, bounds,
70
+ caps, undo windows, dry-run flags. Changing what's allowed never means
71
+ changing the engine.
72
+
73
+ ## Tiers
74
+
75
+ | Tier | Meaning | Example policy |
76
+ |------|---------|----------------|
77
+ | 0 | observe only | reads (exempt from the kill switch) |
78
+ | 1 | auto-execute, reversible, in-bounds | toggle with `compensating_command` + 15-min undo |
79
+ | 2 | auto-execute under cumulative caps | rate + €/day + €/month budgets |
80
+ | 3 | propose-and-confirm (TTL'd approval) | anything irreversible, unlisted, or over cap |
81
+
82
+ ## Documentation
83
+
84
+ Developer guides live in [`docs/`](docs/index.md): the three-minute mental
85
+ model, an integration guide per surface — [library](docs/integration-library.md),
86
+ [HTTP decision service](docs/integration-service.md),
87
+ [MCP proxy](docs/integration-mcp.md),
88
+ [LiteLLM adapter](docs/integration-litellm.md) — and the full
89
+ [policy reference](docs/policy-reference.md).
90
+
91
+ ## Quickstart
92
+
93
+ Requires Python ≥ 3.12.
94
+
95
+ ```bash
96
+ pip install -e ".[dev]"
97
+ pytest # 63 tests — the guardrail suite is the release blocker
98
+ python -m scripts.demo # one of everything, end to end, zero external deps
99
+ ```
100
+
101
+ The demo walks the whole surface: auto-execution and undo, default-deny into a
102
+ real approval that then executes, a bounds rejection, cap exhaustion, dry-run,
103
+ and the kill switch clamping an auto action to propose-and-confirm.
104
+
105
+ ## A policy, concretely
106
+
107
+ ```yaml
108
+ - action_type: ha.set_climate
109
+ tier: 1
110
+ dry_run: true # new action types rehearse first
111
+ compensating_command: ha.restore_climate
112
+ bounds:
113
+ numeric:
114
+ temperature: { min: 17, max: 23 }
115
+ required: [entity_id, temperature]
116
+ strict_params: true
117
+ ```
118
+
119
+ ## v0.2 — the decision/enforcement split, and the engine on other people's doors
120
+
121
+ v0.2 separates the engine into the classic authorization pair — a **Policy
122
+ Decision Point** and **Policy Enforcement Points** — without changing a single
123
+ decision's semantics (the v0.1 suite passes unchanged):
124
+
125
+ - `decision.decide_and_reserve(request, ...)` — Tx A: the full ordered check
126
+ pipeline, cap reservation, and the intent row in the audit log. Returns
127
+ either a terminal result (denied / proposed / dry-run) or a
128
+ `PermittedIntent`: an obligation the caller must enforce.
129
+ - `decision.report_result(intent, ok, ...)` — Tx B: the linked, append-only
130
+ execution receipt, whatever happened.
131
+
132
+ The in-process executor is now literally these two phases composed around a
133
+ connector call. Any other enforcement point — a gateway filter, a tool
134
+ wrapper — composes them around its own act.
135
+
136
+ **The first external enforcement point ships with it: an MCP proxy.**
137
+ `onedoor.mcp.proxy` speaks MCP's stdio transport on both sides: an agent host
138
+ connects to it as if it were the tool server; it spawns the real server as a
139
+ subprocess and forwards everything except `tools/call`, which becomes an
140
+ `ActionRequest` (`mcp.<tool>`) through the full pipeline — unknown tools
141
+ default-deny to a human, bounds are checked before the tool ever sees the
142
+ call, money waits for approval, and the kill switch clamps everything at once.
143
+
144
+ ```bash
145
+ python -m scripts.demo_mcp # an agent's-eye view: 7 calls, every mechanism
146
+ ```
147
+
148
+ This makes the engine usable with agents you don't control: point any MCP
149
+ host at the proxy instead of the tool server, write a policy file, done.
150
+ (The proxy's `onedoor/approve` and `onedoor/kill` JSON-RPC methods are demo
151
+ conveniences, not part of MCP.)
152
+
153
+ ## Using it from an AI gateway (LiteLLM example)
154
+
155
+ `examples/litellm_guardrail.py` is an experimental adapter showing the engine
156
+ as a LiteLLM custom guardrail: `async_pre_call_hook` governs completions
157
+ (model allow-list as *value* bounds, daily caps) and — because LiteLLM routes
158
+ its MCP gateway's tool calls through the same hook (`call_type="call_mcp_tool"`)
159
+ — every MCP tool call, with default-deny, bounds, tier-3 approval and the kill
160
+ switch. Run `python -m examples.litellm_guardrail` for a proxy-free self-test.
161
+ What this adds over the gateway's built-in MCP ACLs: decisions beyond
162
+ allow/deny (defer with an approval id, dry-run), value-level bounds rather
163
+ than parameter-name lists, race-free caps, and an audit row with a reason for
164
+ every decision. `litellm` is not a dependency of this package — the example
165
+ imports it only if you have it.
166
+
167
+ ## The decision service (v0.3)
168
+
169
+ The PDP over HTTP, so any enforcement point in any language can consult the
170
+ engine:
171
+
172
+ ```bash
173
+ pip install "onedoor[service]"
174
+ ONEDOOR_DECIDE_KEYS=dev ONEDOOR_ADMIN_KEYS=root \
175
+ ONEDOOR_POLICIES=config/policies.yaml \
176
+ uvicorn onedoor.service.app:create_app --factory --port 8470
177
+ ```
178
+
179
+ `POST /v1/decide` returns the decision; a permitted one carries an
180
+ `intent_audit_id` — enforce, then `POST /v1/report` the outcome. Approvals,
181
+ denial and the kill switch live under admin-role keys (`ONEDOOR_ADMIN_KEYS`),
182
+ separate from decide-role keys by design: the process that asks for permission
183
+ should not be the process that grants it. Tier-3 proposals can notify a
184
+ webhook (`ONEDOOR_APPROVAL_WEBHOOK`, Slack-compatible payload), and installing
185
+ `onedoor[otel]` lights up OpenTelemetry spans and decision counters with no
186
+ code changes. See `ROADMAP.md` for where this is going (tenancy, Postgres,
187
+ OIDC, audit hardening).
188
+
189
+ ## Origin & status
190
+
191
+ Extracted from a personal single-user control plane (home/energy/money with an
192
+ LLM agent layer), where this engine has governed every action since July 2026 —
193
+ the domain modules stayed home; the engine, its mock connector, its demo action
194
+ types, and its full test suite are what you see here. v0.2: SQLite-backed,
195
+ single-process, synchronous; PDP/PEP split with an MCP proxy as the first
196
+ external enforcement point. Deliberately boring technology; the design is the
197
+ contribution.
198
+
199
+ ## License
200
+
201
+ Apache-2.0.
@@ -0,0 +1,176 @@
1
+ # onedoor
2
+
3
+ **A tiered guardrail engine for agentic systems.**
4
+ The model proposes; the policy layer disposes.
5
+
6
+ Every action in an agentic system — scheduled, rule-fired, LLM-proposed, or
7
+ human-clicked — is a structured `ActionRequest` evaluated by one executor
8
+ against a policy table before anything touches the world. There is one door.
9
+ Nothing else is allowed to call a connector.
10
+
11
+ ```
12
+ kill switch → policy lookup / default-deny → tier-1 integrity (no undo, no
13
+ autonomy) → bounds → dry-run → caps → two-phase execute → append-only audit
14
+ ```
15
+
16
+ ## Why another guardrail project?
17
+
18
+ Most "guardrails" govern what a model may *say*. This engine governs what an
19
+ agent may *do* — and it takes positions most frameworks leave as wishes:
20
+
21
+ - **Default-deny.** An unlisted action type is not an error and not a pass:
22
+ it resolves to propose-and-confirm, with the reason recorded.
23
+ - **Reversibility is a precondition for autonomy.** An auto-tier action whose
24
+ policy declares no compensating command is demoted to human approval at
25
+ runtime — and the policy loader refuses to boot if a Tier-1 entry lacks one.
26
+ Undo is not a feature; it is the admission ticket to auto-execution.
27
+ - **The kill switch outranks everything, including prior consent.** Checked
28
+ before policy lookup; an already-approved action arriving while the switch
29
+ is engaged is blocked (without spawning an approval loop). Reads stay exempt
30
+ — you want visibility *during* the incident.
31
+ - **Bounds are validated before a human ever sees a proposal**, so the
32
+ approval screen can only contain physically sane requests. The human decides
33
+ *whether*, never has to catch *whether it's insane*.
34
+ - **Rehearsal must not spend a real budget.** Dry-run is resolved before cap
35
+ accounting; new action types start in dry-run and log "would have executed".
36
+ - **Caps are reserved race-free** inside the deciding transaction
37
+ (`BEGIN IMMEDIATE`), so two concurrent requests cannot share the last slot.
38
+ - **Two-phase execution.** Tx A decides, reserves caps, and records intent;
39
+ the connector call runs outside any DB lock under a hard timeout; Tx B
40
+ appends the result. A hung smart-plug API cannot hold the engine hostage,
41
+ and a crash leaves an honest "intended, unconfirmed" trail.
42
+ - **The audit log is append-only** — decisions, results, denials, dry-runs,
43
+ and kill-switch blocks, all with typed reason codes, never updated in place.
44
+ - **Policies are data, not code** (`config/policies.yaml`): tiers, bounds,
45
+ caps, undo windows, dry-run flags. Changing what's allowed never means
46
+ changing the engine.
47
+
48
+ ## Tiers
49
+
50
+ | Tier | Meaning | Example policy |
51
+ |------|---------|----------------|
52
+ | 0 | observe only | reads (exempt from the kill switch) |
53
+ | 1 | auto-execute, reversible, in-bounds | toggle with `compensating_command` + 15-min undo |
54
+ | 2 | auto-execute under cumulative caps | rate + €/day + €/month budgets |
55
+ | 3 | propose-and-confirm (TTL'd approval) | anything irreversible, unlisted, or over cap |
56
+
57
+ ## Documentation
58
+
59
+ Developer guides live in [`docs/`](docs/index.md): the three-minute mental
60
+ model, an integration guide per surface — [library](docs/integration-library.md),
61
+ [HTTP decision service](docs/integration-service.md),
62
+ [MCP proxy](docs/integration-mcp.md),
63
+ [LiteLLM adapter](docs/integration-litellm.md) — and the full
64
+ [policy reference](docs/policy-reference.md).
65
+
66
+ ## Quickstart
67
+
68
+ Requires Python ≥ 3.12.
69
+
70
+ ```bash
71
+ pip install -e ".[dev]"
72
+ pytest # 63 tests — the guardrail suite is the release blocker
73
+ python -m scripts.demo # one of everything, end to end, zero external deps
74
+ ```
75
+
76
+ The demo walks the whole surface: auto-execution and undo, default-deny into a
77
+ real approval that then executes, a bounds rejection, cap exhaustion, dry-run,
78
+ and the kill switch clamping an auto action to propose-and-confirm.
79
+
80
+ ## A policy, concretely
81
+
82
+ ```yaml
83
+ - action_type: ha.set_climate
84
+ tier: 1
85
+ dry_run: true # new action types rehearse first
86
+ compensating_command: ha.restore_climate
87
+ bounds:
88
+ numeric:
89
+ temperature: { min: 17, max: 23 }
90
+ required: [entity_id, temperature]
91
+ strict_params: true
92
+ ```
93
+
94
+ ## v0.2 — the decision/enforcement split, and the engine on other people's doors
95
+
96
+ v0.2 separates the engine into the classic authorization pair — a **Policy
97
+ Decision Point** and **Policy Enforcement Points** — without changing a single
98
+ decision's semantics (the v0.1 suite passes unchanged):
99
+
100
+ - `decision.decide_and_reserve(request, ...)` — Tx A: the full ordered check
101
+ pipeline, cap reservation, and the intent row in the audit log. Returns
102
+ either a terminal result (denied / proposed / dry-run) or a
103
+ `PermittedIntent`: an obligation the caller must enforce.
104
+ - `decision.report_result(intent, ok, ...)` — Tx B: the linked, append-only
105
+ execution receipt, whatever happened.
106
+
107
+ The in-process executor is now literally these two phases composed around a
108
+ connector call. Any other enforcement point — a gateway filter, a tool
109
+ wrapper — composes them around its own act.
110
+
111
+ **The first external enforcement point ships with it: an MCP proxy.**
112
+ `onedoor.mcp.proxy` speaks MCP's stdio transport on both sides: an agent host
113
+ connects to it as if it were the tool server; it spawns the real server as a
114
+ subprocess and forwards everything except `tools/call`, which becomes an
115
+ `ActionRequest` (`mcp.<tool>`) through the full pipeline — unknown tools
116
+ default-deny to a human, bounds are checked before the tool ever sees the
117
+ call, money waits for approval, and the kill switch clamps everything at once.
118
+
119
+ ```bash
120
+ python -m scripts.demo_mcp # an agent's-eye view: 7 calls, every mechanism
121
+ ```
122
+
123
+ This makes the engine usable with agents you don't control: point any MCP
124
+ host at the proxy instead of the tool server, write a policy file, done.
125
+ (The proxy's `onedoor/approve` and `onedoor/kill` JSON-RPC methods are demo
126
+ conveniences, not part of MCP.)
127
+
128
+ ## Using it from an AI gateway (LiteLLM example)
129
+
130
+ `examples/litellm_guardrail.py` is an experimental adapter showing the engine
131
+ as a LiteLLM custom guardrail: `async_pre_call_hook` governs completions
132
+ (model allow-list as *value* bounds, daily caps) and — because LiteLLM routes
133
+ its MCP gateway's tool calls through the same hook (`call_type="call_mcp_tool"`)
134
+ — every MCP tool call, with default-deny, bounds, tier-3 approval and the kill
135
+ switch. Run `python -m examples.litellm_guardrail` for a proxy-free self-test.
136
+ What this adds over the gateway's built-in MCP ACLs: decisions beyond
137
+ allow/deny (defer with an approval id, dry-run), value-level bounds rather
138
+ than parameter-name lists, race-free caps, and an audit row with a reason for
139
+ every decision. `litellm` is not a dependency of this package — the example
140
+ imports it only if you have it.
141
+
142
+ ## The decision service (v0.3)
143
+
144
+ The PDP over HTTP, so any enforcement point in any language can consult the
145
+ engine:
146
+
147
+ ```bash
148
+ pip install "onedoor[service]"
149
+ ONEDOOR_DECIDE_KEYS=dev ONEDOOR_ADMIN_KEYS=root \
150
+ ONEDOOR_POLICIES=config/policies.yaml \
151
+ uvicorn onedoor.service.app:create_app --factory --port 8470
152
+ ```
153
+
154
+ `POST /v1/decide` returns the decision; a permitted one carries an
155
+ `intent_audit_id` — enforce, then `POST /v1/report` the outcome. Approvals,
156
+ denial and the kill switch live under admin-role keys (`ONEDOOR_ADMIN_KEYS`),
157
+ separate from decide-role keys by design: the process that asks for permission
158
+ should not be the process that grants it. Tier-3 proposals can notify a
159
+ webhook (`ONEDOOR_APPROVAL_WEBHOOK`, Slack-compatible payload), and installing
160
+ `onedoor[otel]` lights up OpenTelemetry spans and decision counters with no
161
+ code changes. See `ROADMAP.md` for where this is going (tenancy, Postgres,
162
+ OIDC, audit hardening).
163
+
164
+ ## Origin & status
165
+
166
+ Extracted from a personal single-user control plane (home/energy/money with an
167
+ LLM agent layer), where this engine has governed every action since July 2026 —
168
+ the domain modules stayed home; the engine, its mock connector, its demo action
169
+ types, and its full test suite are what you see here. v0.2: SQLite-backed,
170
+ single-process, synchronous; PDP/PEP split with an MCP proxy as the first
171
+ external enforcement point. Deliberately boring technology; the design is the
172
+ contribution.
173
+
174
+ ## License
175
+
176
+ Apache-2.0.
@@ -0,0 +1 @@
1
+ """OneDoor — a tiered guardrail engine for agentic systems. The model proposes; the policy layer disposes."""
@@ -0,0 +1,19 @@
1
+ """Engine settings — the minimal surface the guardrail engine consumes.
2
+
3
+ The parent project carries a much larger Settings object; the engine only ever
4
+ reads these fields (via ``EngineConfig.from_settings``). Override with
5
+ environment variables prefixed ``ONEDOOR_`` or construct directly in code.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pydantic_settings import BaseSettings, SettingsConfigDict
11
+
12
+
13
+ class Settings(BaseSettings):
14
+ model_config = SettingsConfigDict(env_prefix="ONEDOOR_", extra="ignore")
15
+
16
+ db_path: str = "onedoor.db"
17
+ timezone: str = "Europe/Amsterdam"
18
+ approval_ttl_seconds: int = 3600
19
+ connector_timeout_seconds: float = 10.0
@@ -0,0 +1,6 @@
1
+ """Connectors expose read_* (pure reads) and act_* (side effects).
2
+
3
+ act_* functions may only be invoked by the guardrail executor, via the injected
4
+ ConnectorRegistry. In M0 the only connector is the mock used to exercise the
5
+ engine and its tests — no real device/service connectors exist yet.
6
+ """
@@ -0,0 +1,11 @@
1
+ """Connector exceptions. Connectors raise these; schedulers catch and fail soft."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class ConnectorError(Exception):
7
+ """A connector read/act failed (HTTP, timeout, parse)."""
8
+
9
+
10
+ class NotConfigured(ConnectorError):
11
+ """The connector's credentials/URL are not set — skip, don't crash."""
@@ -0,0 +1,46 @@
1
+ """Mock connector — exercises the guardrail engine without any real integration.
2
+
3
+ Registered into a :class:`ConnectorRegistry` for the M0 demo and the test suite.
4
+ The ``act_flaky`` / ``act_slow`` functions exist purely to test fail-soft handling.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import time
10
+
11
+ from onedoor.guardrail.models import JsonValue
12
+ from onedoor.guardrail.registry import ConnectorRegistry
13
+
14
+
15
+ def act_toggle(params: dict[str, JsonValue]) -> dict[str, JsonValue]:
16
+ return {"toggled": True, "target": params.get("target"), "state": params.get("state")}
17
+
18
+
19
+ def act_restore(params: dict[str, JsonValue]) -> dict[str, JsonValue]:
20
+ return {"restored": True, "target": params.get("target")}
21
+
22
+
23
+ def act_ok(params: dict[str, JsonValue]) -> dict[str, JsonValue]:
24
+ return {"ok": True}
25
+
26
+
27
+ def act_flaky(params: dict[str, JsonValue]) -> dict[str, JsonValue]:
28
+ raise RuntimeError("simulated connector failure")
29
+
30
+
31
+ def act_slow(params: dict[str, JsonValue]) -> dict[str, JsonValue]:
32
+ time.sleep(30) # exceeds the connector timeout — used to test timeout handling
33
+ return {"ok": True}
34
+
35
+
36
+ def build_registry() -> ConnectorRegistry:
37
+ """Registry for the M0 demo action types (mirrors config/policies.yaml)."""
38
+ registry = ConnectorRegistry()
39
+ registry.register("demo.toggle", act_toggle)
40
+ registry.register("demo.dry", act_toggle)
41
+ registry.register("demo.capped", act_ok)
42
+ registry.register("demo.restore", act_restore)
43
+ # An unlisted action type (default-deny -> Tier 3) that still has a connector,
44
+ # so approving it actually executes — demonstrating the full approval loop.
45
+ registry.register("demo.unlisted", act_ok)
46
+ return registry