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.
Files changed (24) hide show
  1. policy_as_code_engine-0.1.1/.github/workflows/ci.yml +41 -0
  2. policy_as_code_engine-0.1.1/.github/workflows/publish.yml +35 -0
  3. policy_as_code_engine-0.1.1/.gitignore +12 -0
  4. policy_as_code_engine-0.1.1/LICENSE +21 -0
  5. policy_as_code_engine-0.1.1/PKG-INFO +278 -0
  6. policy_as_code_engine-0.1.1/README.md +235 -0
  7. policy_as_code_engine-0.1.1/examples/example-bundle.yaml +53 -0
  8. policy_as_code_engine-0.1.1/examples/example-context.json +13 -0
  9. policy_as_code_engine-0.1.1/pyproject.toml +91 -0
  10. policy_as_code_engine-0.1.1/src/policy_as_code_engine/__init__.py +50 -0
  11. policy_as_code_engine-0.1.1/src/policy_as_code_engine/__main__.py +53 -0
  12. policy_as_code_engine-0.1.1/src/policy_as_code_engine/app.py +226 -0
  13. policy_as_code_engine-0.1.1/src/policy_as_code_engine/audit_stream.py +82 -0
  14. policy_as_code_engine-0.1.1/src/policy_as_code_engine/evaluator.py +197 -0
  15. policy_as_code_engine-0.1.1/src/policy_as_code_engine/from_decision_card.py +162 -0
  16. policy_as_code_engine-0.1.1/src/policy_as_code_engine/loader.py +46 -0
  17. policy_as_code_engine-0.1.1/src/policy_as_code_engine/models.py +218 -0
  18. policy_as_code_engine-0.1.1/tests/__init__.py +0 -0
  19. policy_as_code_engine-0.1.1/tests/test_app.py +315 -0
  20. policy_as_code_engine-0.1.1/tests/test_audit_stream.py +155 -0
  21. policy_as_code_engine-0.1.1/tests/test_evaluator.py +282 -0
  22. policy_as_code_engine-0.1.1/tests/test_from_decision_card.py +129 -0
  23. policy_as_code_engine-0.1.1/tests/test_loader.py +61 -0
  24. 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,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ .mypy_cache/
8
+ dist/
9
+ build/
10
+ .coverage
11
+ htmlcov/
12
+ .DS_Store
@@ -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
+ [![CI](https://github.com/mizcausevic-dev/policy-as-code-engine/actions/workflows/ci.yml/badge.svg)](https://github.com/mizcausevic-dev/policy-as-code-engine/actions/workflows/ci.yml)
47
+ [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-blue)](https://www.python.org/)
48
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![CI](https://github.com/mizcausevic-dev/policy-as-code-engine/actions/workflows/ci.yml/badge.svg)](https://github.com/mizcausevic-dev/policy-as-code-engine/actions/workflows/ci.yml)
4
+ [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-blue)](https://www.python.org/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
@@ -0,0 +1,13 @@
1
+ {
2
+ "subject": {
3
+ "id": "u-42",
4
+ "role": "admin",
5
+ "suspended": false
6
+ },
7
+ "action": "update",
8
+ "resource": {
9
+ "id": "doc-7",
10
+ "kind": "lesson-plan"
11
+ },
12
+ "data": {}
13
+ }