policy-as-code-engine 0.1.1__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.
- policy_as_code_engine-0.1.1/.github/workflows/ci.yml +41 -0
- policy_as_code_engine-0.1.1/.github/workflows/publish.yml +35 -0
- policy_as_code_engine-0.1.1/.gitignore +12 -0
- policy_as_code_engine-0.1.1/LICENSE +21 -0
- policy_as_code_engine-0.1.1/PKG-INFO +278 -0
- policy_as_code_engine-0.1.1/README.md +235 -0
- policy_as_code_engine-0.1.1/examples/example-bundle.yaml +53 -0
- policy_as_code_engine-0.1.1/examples/example-context.json +13 -0
- policy_as_code_engine-0.1.1/pyproject.toml +91 -0
- policy_as_code_engine-0.1.1/src/policy_as_code_engine/__init__.py +50 -0
- policy_as_code_engine-0.1.1/src/policy_as_code_engine/__main__.py +53 -0
- policy_as_code_engine-0.1.1/src/policy_as_code_engine/app.py +226 -0
- policy_as_code_engine-0.1.1/src/policy_as_code_engine/audit_stream.py +82 -0
- policy_as_code_engine-0.1.1/src/policy_as_code_engine/evaluator.py +197 -0
- policy_as_code_engine-0.1.1/src/policy_as_code_engine/from_decision_card.py +162 -0
- policy_as_code_engine-0.1.1/src/policy_as_code_engine/loader.py +46 -0
- policy_as_code_engine-0.1.1/src/policy_as_code_engine/models.py +218 -0
- policy_as_code_engine-0.1.1/tests/__init__.py +0 -0
- policy_as_code_engine-0.1.1/tests/test_app.py +315 -0
- policy_as_code_engine-0.1.1/tests/test_audit_stream.py +155 -0
- policy_as_code_engine-0.1.1/tests/test_evaluator.py +282 -0
- policy_as_code_engine-0.1.1/tests/test_from_decision_card.py +129 -0
- policy_as_code_engine-0.1.1/tests/test_loader.py +61 -0
- policy_as_code_engine-0.1.1/tests/test_models.py +125 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
name: test (py${{ matrix.python-version }})
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: ${{ matrix.python-version }}
|
|
24
|
+
cache: pip
|
|
25
|
+
|
|
26
|
+
- name: Install
|
|
27
|
+
run: |
|
|
28
|
+
python -m pip install --upgrade pip
|
|
29
|
+
pip install -e ".[dev]"
|
|
30
|
+
|
|
31
|
+
- name: Lint
|
|
32
|
+
run: ruff check src tests
|
|
33
|
+
|
|
34
|
+
- name: Format check
|
|
35
|
+
run: ruff format --check src tests
|
|
36
|
+
|
|
37
|
+
- name: Type check
|
|
38
|
+
run: mypy src
|
|
39
|
+
|
|
40
|
+
- name: Test
|
|
41
|
+
run: pytest -v
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
name: Build + publish wheel + sdist
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment:
|
|
12
|
+
name: pypi
|
|
13
|
+
url: https://pypi.org/p/policy-as-code-engine
|
|
14
|
+
permissions:
|
|
15
|
+
id-token: write
|
|
16
|
+
contents: read
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
with:
|
|
20
|
+
fetch-depth: 0
|
|
21
|
+
- uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: "3.13"
|
|
24
|
+
- name: Install build tools
|
|
25
|
+
run: |
|
|
26
|
+
python -m pip install --upgrade pip
|
|
27
|
+
python -m pip install build
|
|
28
|
+
- name: Build distributions
|
|
29
|
+
run: python -m build
|
|
30
|
+
- name: Inspect distributions
|
|
31
|
+
run: ls -la dist/
|
|
32
|
+
- name: Publish to PyPI
|
|
33
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
34
|
+
with:
|
|
35
|
+
attestations: true
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Miz Causevic / Kinetic Gain
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: policy-as-code-engine
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Declarative policy-as-code evaluator. JSON/YAML rules + Python predicates over arbitrary context objects. Enforces AI Procurement Decision Card conditions at request time. Optional audit-stream-py integration via AUDIT_STREAM_URL.
|
|
5
|
+
Project-URL: Homepage, https://github.com/mizcausevic-dev/policy-as-code-engine
|
|
6
|
+
Project-URL: Repository, https://github.com/mizcausevic-dev/policy-as-code-engine
|
|
7
|
+
Project-URL: Issues, https://github.com/mizcausevic-dev/policy-as-code-engine/issues
|
|
8
|
+
Project-URL: Decision Card spec, https://github.com/mizcausevic-dev/ai-procurement-decision-spec
|
|
9
|
+
Project-URL: Author Site, https://kineticgain.com/
|
|
10
|
+
Author-email: Miz Causevic <miz@kineticgain.com>
|
|
11
|
+
License: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: ai-governance,decision-intelligence,fastapi,kinetic-gain-protocol-suite,policy,policy-as-code,rules-engine
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Framework :: FastAPI
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Intended Audience :: System Administrators
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.11
|
|
27
|
+
Requires-Dist: httpx>=0.27
|
|
28
|
+
Requires-Dist: pydantic>=2.7
|
|
29
|
+
Requires-Dist: pyyaml>=6.0
|
|
30
|
+
Provides-Extra: api
|
|
31
|
+
Requires-Dist: fastapi>=0.115; extra == 'api'
|
|
32
|
+
Requires-Dist: uvicorn[standard]>=0.30; extra == 'api'
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: fastapi>=0.115; extra == 'dev'
|
|
35
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
36
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest>=8.2; extra == 'dev'
|
|
39
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
40
|
+
Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
|
|
41
|
+
Requires-Dist: uvicorn[standard]>=0.30; extra == 'dev'
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
|
|
44
|
+
# policy-as-code-engine
|
|
45
|
+
|
|
46
|
+
[](https://github.com/mizcausevic-dev/policy-as-code-engine/actions/workflows/ci.yml)
|
|
47
|
+
[](https://www.python.org/)
|
|
48
|
+
[](LICENSE)
|
|
49
|
+
|
|
50
|
+
**Declarative policy-as-code evaluator for Python services.** JSON/YAML rules → first-match-wins evaluation → structured allow/deny decision with the matching rule and the reason. Cheap to embed; ships with a FastAPI surface; **pairs directly with [`procurement-decision-api`](https://github.com/mizcausevic-dev/procurement-decision-api)** so the same Decision Card that records a buyer's posture also becomes the runtime gate that enforces it.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Why
|
|
55
|
+
|
|
56
|
+
Most policy engines either ask you to learn a DSL (Rego, Cedar) or hand you a dictionary-of-lambdas and call it a library. Neither is the right shape when the *source of truth* is a JSON document a human signed off on. This engine:
|
|
57
|
+
|
|
58
|
+
1. **Reads JSON/YAML bundles.** No DSL. The matcher tree is the policy.
|
|
59
|
+
2. **Returns *why*, not just *what*.** Every decision carries the matched policy + rule + reason. Operators get a real audit log on each evaluation.
|
|
60
|
+
3. **Bridges to the Kinetic Gain Protocol Suite.** A single endpoint turns an AI Procurement Decision Card into a runtime-enforceable `PolicyBundle` — approve, reject, or approve-with-conditions all map to concrete allow/deny logic.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install policy-as-code-engine
|
|
68
|
+
# with the FastAPI surface:
|
|
69
|
+
pip install "policy-as-code-engine[api]"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Python 3.11+. Runtime deps: `pydantic` + `PyYAML`.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Library quickstart
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from policy_as_code_engine import (
|
|
80
|
+
EvaluationContext,
|
|
81
|
+
PolicyBundle,
|
|
82
|
+
PolicyEvaluator,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
bundle = PolicyBundle.model_validate({
|
|
86
|
+
"bundle_id": "edu-gate",
|
|
87
|
+
"policies": [{
|
|
88
|
+
"id": "writes-require-admin",
|
|
89
|
+
"default_effect": "deny",
|
|
90
|
+
"rules": [
|
|
91
|
+
{
|
|
92
|
+
"id": "admin-writes",
|
|
93
|
+
"effect": "allow",
|
|
94
|
+
"when": {
|
|
95
|
+
"kind": "all_of",
|
|
96
|
+
"matchers": [
|
|
97
|
+
{"kind": "in", "field": "action", "value": ["create", "update", "delete"]},
|
|
98
|
+
{"kind": "eq", "field": "subject.role", "value": "admin"},
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
}],
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
ctx = EvaluationContext(
|
|
107
|
+
subject={"id": "u-42", "role": "admin"},
|
|
108
|
+
action="update",
|
|
109
|
+
resource={"id": "doc-7"},
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
result = PolicyEvaluator().evaluate(bundle, ctx)
|
|
113
|
+
print(result.decision.kind) # "allow"
|
|
114
|
+
print(result.decision.matched_rule_id) # "admin-writes"
|
|
115
|
+
print(result.decision.reason) # "matched rule 'admin-writes'"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`result.policy_decisions` carries every per-policy outcome — drop it straight into your audit log.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Bundle DSL
|
|
123
|
+
|
|
124
|
+
A bundle is a small recursive structure. Matchers compose; rules ordered.
|
|
125
|
+
|
|
126
|
+
### Field matchers
|
|
127
|
+
|
|
128
|
+
| Kind | Notes |
|
|
129
|
+
| --------------- | --- |
|
|
130
|
+
| `eq` / `ne` | Strict equality. |
|
|
131
|
+
| `gt` / `gte` / `lt` / `lte` | Comparison; returns `false` on incompatible types (won't raise). |
|
|
132
|
+
| `in` / `not_in` | `value` must be a list. |
|
|
133
|
+
| `contains` | Works against strings, lists, sets, dicts. |
|
|
134
|
+
| `exists` / `missing` | No `value`. Operates against the dotted-path resolver. |
|
|
135
|
+
| `regex` | Compiled patterns are cached per-evaluator. |
|
|
136
|
+
| `starts_with` / `ends_with` | String-only. |
|
|
137
|
+
|
|
138
|
+
### Composite matchers
|
|
139
|
+
|
|
140
|
+
| Kind | Children | Truth |
|
|
141
|
+
| -------- | -------- | --- |
|
|
142
|
+
| `all_of` | `matchers: [...]` | All children true. |
|
|
143
|
+
| `any_of` | `matchers: [...]` | At least one child true. |
|
|
144
|
+
| `not` | `matcher: {...}` | Inverts the child. |
|
|
145
|
+
| `always` | — | Always true. Useful as a final catch-all. |
|
|
146
|
+
|
|
147
|
+
### Dotted paths
|
|
148
|
+
|
|
149
|
+
The resolver looks at the merged context (`data` + `subject` + `action` + `resource`):
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
subject.role
|
|
153
|
+
resource.tags.0 # list index
|
|
154
|
+
data.conditions_satisfied.dpa-signed
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Missing segments produce a `_MISSING` sentinel — `exists` / `missing` matchers see it; every other matcher returns `false`.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## FastAPI surface
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
pip install "policy-as-code-engine[api]"
|
|
165
|
+
python -m policy_as_code_engine # binds 0.0.0.0:8089 by default
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
| Method | Path | What it does |
|
|
169
|
+
| --- | --- | --- |
|
|
170
|
+
| GET | `/healthz` | Liveness probe. |
|
|
171
|
+
| GET | `/` | Service info. |
|
|
172
|
+
| POST | `/bundles` | Register a `PolicyBundle` in memory. |
|
|
173
|
+
| GET | `/bundles` | List registered bundle IDs. |
|
|
174
|
+
| GET | `/bundles/{bundle_id}` | Inspect a registered bundle. |
|
|
175
|
+
| POST | `/bundles/{bundle_id}/evaluate` | Evaluate a stored bundle against an `EvaluationContext`. |
|
|
176
|
+
| POST | `/evaluate` | One-shot. Bundle + context in, decision out. |
|
|
177
|
+
| POST | `/bundles/from-decision-card` | **The cross-ecosystem hook.** Turn a Kinetic Gain Procurement Decision Card into a `PolicyBundle` and register it. |
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## The cross-ecosystem hook
|
|
182
|
+
|
|
183
|
+
The headline feature. An AI Procurement Decision Card is the buyer-side record that says "we evaluated this vendor and our position is X." This engine turns that human-authored artifact into a runtime gate, mechanically.
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
curl -X POST http://localhost:8089/bundles/from-decision-card \
|
|
187
|
+
-H 'Content-Type: application/json' \
|
|
188
|
+
-d @decision-card.json
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Mapping:
|
|
192
|
+
|
|
193
|
+
| Decision Card status | Resulting bundle |
|
|
194
|
+
| --- | --- |
|
|
195
|
+
| `approved` | Single `allow-all` policy. |
|
|
196
|
+
| `rejected` · `rejected-with-remediation` · `withdrawn` · `expired` · `pending` | Single `deny-all` policy (fail safe). |
|
|
197
|
+
| `approved-with-conditions` | One policy *per* condition. Each policy `allow`s only when `conditions_satisfied.{condition_id}` is `true` in the evaluation context; `deny` otherwise. The bundle combiner does deny-trumps-allow, so **every** condition must be satisfied to allow. |
|
|
198
|
+
|
|
199
|
+
Wire your own satisfaction signal — DPA verifier, bias-audit freshness check, attestation timestamp — into the context, and the bundle does the rest.
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from policy_as_code_engine import (
|
|
203
|
+
EvaluationContext,
|
|
204
|
+
PolicyEvaluator,
|
|
205
|
+
policy_bundle_from_decision_card,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
card = {...} # POST /decisions/draft output from procurement-decision-api
|
|
209
|
+
bundle = policy_bundle_from_decision_card(card)
|
|
210
|
+
|
|
211
|
+
ctx = EvaluationContext(
|
|
212
|
+
subject={"id": "u-1"},
|
|
213
|
+
action="enroll",
|
|
214
|
+
data={
|
|
215
|
+
"conditions_satisfied": {
|
|
216
|
+
"dpa-signed": True,
|
|
217
|
+
"bias-audit-fresh": True,
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
decision = PolicyEvaluator().evaluate(bundle, ctx).decision
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## CLI
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
python -m policy_as_code_engine eval examples/example-bundle.yaml examples/example-context.json
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Prints the full `EvaluationResult` as JSON. Exits non-zero on `deny`.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## How decisions combine
|
|
238
|
+
|
|
239
|
+
Inside a single policy: **first matching rule wins**, otherwise `default_effect`.
|
|
240
|
+
|
|
241
|
+
Across the bundle:
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
deny -> deny (any policy denies => bundle denies)
|
|
245
|
+
allow -> allow (otherwise, any allow => bundle allows)
|
|
246
|
+
neither -> not_applicable
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Per-policy decisions are always returned — useful for "we denied because of policy B, but A would have allowed" audit narratives.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Tests
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
pip install -e ".[dev]"
|
|
257
|
+
ruff check src tests && ruff format --check src tests
|
|
258
|
+
mypy src
|
|
259
|
+
pytest -v
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
CI matrix runs Python 3.11 / 3.12 / 3.13.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Related in this ecosystem
|
|
267
|
+
|
|
268
|
+
- **[procurement-decision-api](https://github.com/mizcausevic-dev/procurement-decision-api)** — drafts the Decision Cards that this engine enforces.
|
|
269
|
+
- **[ai-procurement-decision-spec](https://github.com/mizcausevic-dev/ai-procurement-decision-spec)** — the v0.1 schema.
|
|
270
|
+
- **[slo-budget-tracker](https://github.com/mizcausevic-dev/slo-budget-tracker)** — error-budget tracker that you can wire into the same FastAPI app.
|
|
271
|
+
- **[reliability-toolkit-rs](https://github.com/mizcausevic-dev/reliability-toolkit-rs)** — Rust async reliability primitives.
|
|
272
|
+
- More at [kineticgain.com](https://kineticgain.com/).
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## License
|
|
277
|
+
|
|
278
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# policy-as-code-engine
|
|
2
|
+
|
|
3
|
+
[](https://github.com/mizcausevic-dev/policy-as-code-engine/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.python.org/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
**Declarative policy-as-code evaluator for Python services.** JSON/YAML rules → first-match-wins evaluation → structured allow/deny decision with the matching rule and the reason. Cheap to embed; ships with a FastAPI surface; **pairs directly with [`procurement-decision-api`](https://github.com/mizcausevic-dev/procurement-decision-api)** so the same Decision Card that records a buyer's posture also becomes the runtime gate that enforces it.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
Most policy engines either ask you to learn a DSL (Rego, Cedar) or hand you a dictionary-of-lambdas and call it a library. Neither is the right shape when the *source of truth* is a JSON document a human signed off on. This engine:
|
|
14
|
+
|
|
15
|
+
1. **Reads JSON/YAML bundles.** No DSL. The matcher tree is the policy.
|
|
16
|
+
2. **Returns *why*, not just *what*.** Every decision carries the matched policy + rule + reason. Operators get a real audit log on each evaluation.
|
|
17
|
+
3. **Bridges to the Kinetic Gain Protocol Suite.** A single endpoint turns an AI Procurement Decision Card into a runtime-enforceable `PolicyBundle` — approve, reject, or approve-with-conditions all map to concrete allow/deny logic.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install policy-as-code-engine
|
|
25
|
+
# with the FastAPI surface:
|
|
26
|
+
pip install "policy-as-code-engine[api]"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Python 3.11+. Runtime deps: `pydantic` + `PyYAML`.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Library quickstart
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from policy_as_code_engine import (
|
|
37
|
+
EvaluationContext,
|
|
38
|
+
PolicyBundle,
|
|
39
|
+
PolicyEvaluator,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
bundle = PolicyBundle.model_validate({
|
|
43
|
+
"bundle_id": "edu-gate",
|
|
44
|
+
"policies": [{
|
|
45
|
+
"id": "writes-require-admin",
|
|
46
|
+
"default_effect": "deny",
|
|
47
|
+
"rules": [
|
|
48
|
+
{
|
|
49
|
+
"id": "admin-writes",
|
|
50
|
+
"effect": "allow",
|
|
51
|
+
"when": {
|
|
52
|
+
"kind": "all_of",
|
|
53
|
+
"matchers": [
|
|
54
|
+
{"kind": "in", "field": "action", "value": ["create", "update", "delete"]},
|
|
55
|
+
{"kind": "eq", "field": "subject.role", "value": "admin"},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
}],
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
ctx = EvaluationContext(
|
|
64
|
+
subject={"id": "u-42", "role": "admin"},
|
|
65
|
+
action="update",
|
|
66
|
+
resource={"id": "doc-7"},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
result = PolicyEvaluator().evaluate(bundle, ctx)
|
|
70
|
+
print(result.decision.kind) # "allow"
|
|
71
|
+
print(result.decision.matched_rule_id) # "admin-writes"
|
|
72
|
+
print(result.decision.reason) # "matched rule 'admin-writes'"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`result.policy_decisions` carries every per-policy outcome — drop it straight into your audit log.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Bundle DSL
|
|
80
|
+
|
|
81
|
+
A bundle is a small recursive structure. Matchers compose; rules ordered.
|
|
82
|
+
|
|
83
|
+
### Field matchers
|
|
84
|
+
|
|
85
|
+
| Kind | Notes |
|
|
86
|
+
| --------------- | --- |
|
|
87
|
+
| `eq` / `ne` | Strict equality. |
|
|
88
|
+
| `gt` / `gte` / `lt` / `lte` | Comparison; returns `false` on incompatible types (won't raise). |
|
|
89
|
+
| `in` / `not_in` | `value` must be a list. |
|
|
90
|
+
| `contains` | Works against strings, lists, sets, dicts. |
|
|
91
|
+
| `exists` / `missing` | No `value`. Operates against the dotted-path resolver. |
|
|
92
|
+
| `regex` | Compiled patterns are cached per-evaluator. |
|
|
93
|
+
| `starts_with` / `ends_with` | String-only. |
|
|
94
|
+
|
|
95
|
+
### Composite matchers
|
|
96
|
+
|
|
97
|
+
| Kind | Children | Truth |
|
|
98
|
+
| -------- | -------- | --- |
|
|
99
|
+
| `all_of` | `matchers: [...]` | All children true. |
|
|
100
|
+
| `any_of` | `matchers: [...]` | At least one child true. |
|
|
101
|
+
| `not` | `matcher: {...}` | Inverts the child. |
|
|
102
|
+
| `always` | — | Always true. Useful as a final catch-all. |
|
|
103
|
+
|
|
104
|
+
### Dotted paths
|
|
105
|
+
|
|
106
|
+
The resolver looks at the merged context (`data` + `subject` + `action` + `resource`):
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
subject.role
|
|
110
|
+
resource.tags.0 # list index
|
|
111
|
+
data.conditions_satisfied.dpa-signed
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Missing segments produce a `_MISSING` sentinel — `exists` / `missing` matchers see it; every other matcher returns `false`.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## FastAPI surface
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
pip install "policy-as-code-engine[api]"
|
|
122
|
+
python -m policy_as_code_engine # binds 0.0.0.0:8089 by default
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
| Method | Path | What it does |
|
|
126
|
+
| --- | --- | --- |
|
|
127
|
+
| GET | `/healthz` | Liveness probe. |
|
|
128
|
+
| GET | `/` | Service info. |
|
|
129
|
+
| POST | `/bundles` | Register a `PolicyBundle` in memory. |
|
|
130
|
+
| GET | `/bundles` | List registered bundle IDs. |
|
|
131
|
+
| GET | `/bundles/{bundle_id}` | Inspect a registered bundle. |
|
|
132
|
+
| POST | `/bundles/{bundle_id}/evaluate` | Evaluate a stored bundle against an `EvaluationContext`. |
|
|
133
|
+
| POST | `/evaluate` | One-shot. Bundle + context in, decision out. |
|
|
134
|
+
| POST | `/bundles/from-decision-card` | **The cross-ecosystem hook.** Turn a Kinetic Gain Procurement Decision Card into a `PolicyBundle` and register it. |
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## The cross-ecosystem hook
|
|
139
|
+
|
|
140
|
+
The headline feature. An AI Procurement Decision Card is the buyer-side record that says "we evaluated this vendor and our position is X." This engine turns that human-authored artifact into a runtime gate, mechanically.
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
curl -X POST http://localhost:8089/bundles/from-decision-card \
|
|
144
|
+
-H 'Content-Type: application/json' \
|
|
145
|
+
-d @decision-card.json
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Mapping:
|
|
149
|
+
|
|
150
|
+
| Decision Card status | Resulting bundle |
|
|
151
|
+
| --- | --- |
|
|
152
|
+
| `approved` | Single `allow-all` policy. |
|
|
153
|
+
| `rejected` · `rejected-with-remediation` · `withdrawn` · `expired` · `pending` | Single `deny-all` policy (fail safe). |
|
|
154
|
+
| `approved-with-conditions` | One policy *per* condition. Each policy `allow`s only when `conditions_satisfied.{condition_id}` is `true` in the evaluation context; `deny` otherwise. The bundle combiner does deny-trumps-allow, so **every** condition must be satisfied to allow. |
|
|
155
|
+
|
|
156
|
+
Wire your own satisfaction signal — DPA verifier, bias-audit freshness check, attestation timestamp — into the context, and the bundle does the rest.
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from policy_as_code_engine import (
|
|
160
|
+
EvaluationContext,
|
|
161
|
+
PolicyEvaluator,
|
|
162
|
+
policy_bundle_from_decision_card,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
card = {...} # POST /decisions/draft output from procurement-decision-api
|
|
166
|
+
bundle = policy_bundle_from_decision_card(card)
|
|
167
|
+
|
|
168
|
+
ctx = EvaluationContext(
|
|
169
|
+
subject={"id": "u-1"},
|
|
170
|
+
action="enroll",
|
|
171
|
+
data={
|
|
172
|
+
"conditions_satisfied": {
|
|
173
|
+
"dpa-signed": True,
|
|
174
|
+
"bias-audit-fresh": True,
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
decision = PolicyEvaluator().evaluate(bundle, ctx).decision
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## CLI
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
python -m policy_as_code_engine eval examples/example-bundle.yaml examples/example-context.json
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Prints the full `EvaluationResult` as JSON. Exits non-zero on `deny`.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## How decisions combine
|
|
195
|
+
|
|
196
|
+
Inside a single policy: **first matching rule wins**, otherwise `default_effect`.
|
|
197
|
+
|
|
198
|
+
Across the bundle:
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
deny -> deny (any policy denies => bundle denies)
|
|
202
|
+
allow -> allow (otherwise, any allow => bundle allows)
|
|
203
|
+
neither -> not_applicable
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Per-policy decisions are always returned — useful for "we denied because of policy B, but A would have allowed" audit narratives.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Tests
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
pip install -e ".[dev]"
|
|
214
|
+
ruff check src tests && ruff format --check src tests
|
|
215
|
+
mypy src
|
|
216
|
+
pytest -v
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
CI matrix runs Python 3.11 / 3.12 / 3.13.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Related in this ecosystem
|
|
224
|
+
|
|
225
|
+
- **[procurement-decision-api](https://github.com/mizcausevic-dev/procurement-decision-api)** — drafts the Decision Cards that this engine enforces.
|
|
226
|
+
- **[ai-procurement-decision-spec](https://github.com/mizcausevic-dev/ai-procurement-decision-spec)** — the v0.1 schema.
|
|
227
|
+
- **[slo-budget-tracker](https://github.com/mizcausevic-dev/slo-budget-tracker)** — error-budget tracker that you can wire into the same FastAPI app.
|
|
228
|
+
- **[reliability-toolkit-rs](https://github.com/mizcausevic-dev/reliability-toolkit-rs)** — Rust async reliability primitives.
|
|
229
|
+
- More at [kineticgain.com](https://kineticgain.com/).
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
bundle_id: edu-vendor-gate
|
|
2
|
+
version: "0.1.0"
|
|
3
|
+
description: "Two-policy gate: admin-only writes, plus a global block for suspended subjects."
|
|
4
|
+
|
|
5
|
+
policies:
|
|
6
|
+
- id: writes-require-admin
|
|
7
|
+
description: "Mutating actions require an admin role."
|
|
8
|
+
default_effect: allow
|
|
9
|
+
rules:
|
|
10
|
+
- id: non-write-passthrough
|
|
11
|
+
effect: allow
|
|
12
|
+
description: "GET-like actions don't require admin."
|
|
13
|
+
when:
|
|
14
|
+
kind: in
|
|
15
|
+
field: action
|
|
16
|
+
value: ["read", "list", "get"]
|
|
17
|
+
- id: admin-write-allowed
|
|
18
|
+
effect: allow
|
|
19
|
+
when:
|
|
20
|
+
kind: all_of
|
|
21
|
+
matchers:
|
|
22
|
+
- kind: in
|
|
23
|
+
field: action
|
|
24
|
+
value: ["create", "update", "delete"]
|
|
25
|
+
- kind: eq
|
|
26
|
+
field: subject.role
|
|
27
|
+
value: admin
|
|
28
|
+
- id: non-admin-write-denied
|
|
29
|
+
effect: deny
|
|
30
|
+
description: "Writes by non-admins are blocked."
|
|
31
|
+
when:
|
|
32
|
+
kind: any_of
|
|
33
|
+
matchers:
|
|
34
|
+
- kind: eq
|
|
35
|
+
field: action
|
|
36
|
+
value: create
|
|
37
|
+
- kind: eq
|
|
38
|
+
field: action
|
|
39
|
+
value: update
|
|
40
|
+
- kind: eq
|
|
41
|
+
field: action
|
|
42
|
+
value: delete
|
|
43
|
+
|
|
44
|
+
- id: deny-suspended
|
|
45
|
+
description: "Suspended subjects are blocked outright."
|
|
46
|
+
default_effect: allow
|
|
47
|
+
rules:
|
|
48
|
+
- id: suspended-deny
|
|
49
|
+
effect: deny
|
|
50
|
+
when:
|
|
51
|
+
kind: eq
|
|
52
|
+
field: subject.suspended
|
|
53
|
+
value: true
|