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.
- onedoor-0.3.0/LICENSE +202 -0
- onedoor-0.3.0/PKG-INFO +201 -0
- onedoor-0.3.0/README.md +176 -0
- onedoor-0.3.0/onedoor/__init__.py +1 -0
- onedoor-0.3.0/onedoor/config.py +19 -0
- onedoor-0.3.0/onedoor/connectors/__init__.py +6 -0
- onedoor-0.3.0/onedoor/connectors/errors.py +11 -0
- onedoor-0.3.0/onedoor/connectors/mock.py +46 -0
- onedoor-0.3.0/onedoor/guardrail/__init__.py +56 -0
- onedoor-0.3.0/onedoor/guardrail/approvals.py +98 -0
- onedoor-0.3.0/onedoor/guardrail/audit.py +130 -0
- onedoor-0.3.0/onedoor/guardrail/bounds.py +49 -0
- onedoor-0.3.0/onedoor/guardrail/caps.py +103 -0
- onedoor-0.3.0/onedoor/guardrail/decision.py +312 -0
- onedoor-0.3.0/onedoor/guardrail/errors.py +35 -0
- onedoor-0.3.0/onedoor/guardrail/executor.py +207 -0
- onedoor-0.3.0/onedoor/guardrail/killswitch.py +34 -0
- onedoor-0.3.0/onedoor/guardrail/models.py +169 -0
- onedoor-0.3.0/onedoor/guardrail/policy.py +51 -0
- onedoor-0.3.0/onedoor/guardrail/policy_loader.py +73 -0
- onedoor-0.3.0/onedoor/guardrail/registry.py +29 -0
- onedoor-0.3.0/onedoor/guardrail/undo.py +79 -0
- onedoor-0.3.0/onedoor/mcp/__init__.py +1 -0
- onedoor-0.3.0/onedoor/mcp/demo_server.py +100 -0
- onedoor-0.3.0/onedoor/mcp/proxy.py +235 -0
- onedoor-0.3.0/onedoor/service/__init__.py +1 -0
- onedoor-0.3.0/onedoor/service/app.py +270 -0
- onedoor-0.3.0/onedoor/service/notify.py +70 -0
- onedoor-0.3.0/onedoor/service/telemetry.py +48 -0
- onedoor-0.3.0/onedoor/store/__init__.py +1 -0
- onedoor-0.3.0/onedoor/store/bus.py +44 -0
- onedoor-0.3.0/onedoor/store/clock.py +28 -0
- onedoor-0.3.0/onedoor/store/db.py +93 -0
- onedoor-0.3.0/onedoor.egg-info/PKG-INFO +201 -0
- onedoor-0.3.0/onedoor.egg-info/SOURCES.txt +38 -0
- onedoor-0.3.0/onedoor.egg-info/dependency_links.txt +1 -0
- onedoor-0.3.0/onedoor.egg-info/requires.txt +17 -0
- onedoor-0.3.0/onedoor.egg-info/top_level.txt +1 -0
- onedoor-0.3.0/pyproject.toml +31 -0
- 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.
|
onedoor-0.3.0/README.md
ADDED
|
@@ -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
|