policy-as-code-engine 0.1.1__tar.gz → 0.2.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.
- policy_as_code_engine-0.2.0/PKG-INFO +294 -0
- policy_as_code_engine-0.2.0/README.md +247 -0
- {policy_as_code_engine-0.1.1 → policy_as_code_engine-0.2.0}/pyproject.toml +10 -3
- {policy_as_code_engine-0.1.1 → policy_as_code_engine-0.2.0}/src/policy_as_code_engine/__init__.py +1 -1
- {policy_as_code_engine-0.1.1 → policy_as_code_engine-0.2.0}/src/policy_as_code_engine/__main__.py +2 -2
- policy_as_code_engine-0.2.0/src/policy_as_code_engine/app.py +503 -0
- {policy_as_code_engine-0.1.1 → policy_as_code_engine-0.2.0}/src/policy_as_code_engine/audit_stream.py +5 -2
- policy_as_code_engine-0.2.0/src/policy_as_code_engine/card_attestation.py +72 -0
- {policy_as_code_engine-0.1.1 → policy_as_code_engine-0.2.0}/src/policy_as_code_engine/evaluator.py +42 -5
- policy_as_code_engine-0.2.0/src/policy_as_code_engine/from_decision_card.py +368 -0
- {policy_as_code_engine-0.1.1 → policy_as_code_engine-0.2.0}/src/policy_as_code_engine/models.py +81 -11
- policy_as_code_engine-0.2.0/src/policy_as_code_engine/py.typed +1 -0
- policy_as_code_engine-0.2.0/tests/test_app.py +639 -0
- {policy_as_code_engine-0.1.1 → policy_as_code_engine-0.2.0}/tests/test_audit_stream.py +18 -3
- policy_as_code_engine-0.2.0/tests/test_card_attestation.py +57 -0
- {policy_as_code_engine-0.1.1 → policy_as_code_engine-0.2.0}/tests/test_evaluator.py +41 -0
- policy_as_code_engine-0.2.0/tests/test_from_decision_card.py +294 -0
- {policy_as_code_engine-0.1.1 → policy_as_code_engine-0.2.0}/tests/test_models.py +20 -0
- policy_as_code_engine-0.1.1/.github/workflows/ci.yml +0 -41
- policy_as_code_engine-0.1.1/.github/workflows/publish.yml +0 -35
- policy_as_code_engine-0.1.1/PKG-INFO +0 -278
- policy_as_code_engine-0.1.1/README.md +0 -235
- policy_as_code_engine-0.1.1/src/policy_as_code_engine/app.py +0 -226
- policy_as_code_engine-0.1.1/src/policy_as_code_engine/from_decision_card.py +0 -162
- policy_as_code_engine-0.1.1/tests/test_app.py +0 -315
- policy_as_code_engine-0.1.1/tests/test_from_decision_card.py +0 -129
- {policy_as_code_engine-0.1.1 → policy_as_code_engine-0.2.0}/.gitignore +0 -0
- {policy_as_code_engine-0.1.1 → policy_as_code_engine-0.2.0}/LICENSE +0 -0
- {policy_as_code_engine-0.1.1 → policy_as_code_engine-0.2.0}/examples/example-bundle.yaml +0 -0
- {policy_as_code_engine-0.1.1 → policy_as_code_engine-0.2.0}/examples/example-context.json +0 -0
- {policy_as_code_engine-0.1.1 → policy_as_code_engine-0.2.0}/src/policy_as_code_engine/loader.py +0 -0
- {policy_as_code_engine-0.1.1 → policy_as_code_engine-0.2.0}/tests/__init__.py +0 -0
- {policy_as_code_engine-0.1.1 → policy_as_code_engine-0.2.0}/tests/test_loader.py +0 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: policy-as-code-engine
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Declarative policy evaluator with Decision Card field conversion and optional best-effort audit events.
|
|
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: cryptography<51,>=50.0.2
|
|
28
|
+
Requires-Dist: httpx>=0.27
|
|
29
|
+
Requires-Dist: pydantic>=2.7
|
|
30
|
+
Requires-Dist: pyyaml>=6.0
|
|
31
|
+
Requires-Dist: regex<2027,>=2024.11.6
|
|
32
|
+
Requires-Dist: rfc8785<0.2,>=0.1.4
|
|
33
|
+
Provides-Extra: api
|
|
34
|
+
Requires-Dist: fastapi>=0.115; extra == 'api'
|
|
35
|
+
Requires-Dist: uvicorn[standard]>=0.30; extra == 'api'
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: fastapi>=0.115; extra == 'dev'
|
|
38
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
39
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
40
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
41
|
+
Requires-Dist: pytest>=8.2; extra == 'dev'
|
|
42
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
43
|
+
Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
|
|
44
|
+
Requires-Dist: types-regex>=2024.11.6; extra == 'dev'
|
|
45
|
+
Requires-Dist: uvicorn[standard]>=0.30; extra == 'dev'
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
|
|
48
|
+
# policy-as-code-engine
|
|
49
|
+
|
|
50
|
+
[](https://github.com/mizcausevic-dev/policy-as-code-engine/actions/workflows/ci.yml)
|
|
51
|
+
[](https://www.python.org/)
|
|
52
|
+
[](LICENSE)
|
|
53
|
+
|
|
54
|
+
**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. The HTTP bridge converts an operator-trusted, attested v0.1 Decision Card into a vendor-scoped `use` bundle after verifying its v2 signature against a pinned key. The operator must verify buyer authority and condition evidence separately.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Why
|
|
59
|
+
|
|
60
|
+
The engine keeps policy rules in JSON/YAML and returns the reason for each decision. Its Decision Card bridge is designed for cards that have completed buyer review and an independent key-trust process. It:
|
|
61
|
+
|
|
62
|
+
1. **Reads JSON/YAML bundles.** No DSL. The matcher tree is the policy.
|
|
63
|
+
2. **Returns *why*, not just *what*.** Every decision carries the matched policy + rule + reason. The library does not persist an audit log; optional outbound audit events are best effort.
|
|
64
|
+
3. **Bridges to the Kinetic Gain Protocol Suite.** Positive card conversion verifies a buyer-key attestation; the caller must pin the buyer key independently. The HTTP adapter also keeps condition assertions behind the admin credential, with a seven-day maximum validity.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Install
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install policy-as-code-engine
|
|
72
|
+
# with the FastAPI surface:
|
|
73
|
+
pip install "policy-as-code-engine[api]"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Python 3.11+. Runtime deps: `pydantic`, `PyYAML`, `httpx`, `cryptography`, `rfc8785`, and `regex`. The last three provide Ed25519 verification, cross-language canonical JSON, and bounded regex matching.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Library quickstart
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from policy_as_code_engine import (
|
|
84
|
+
EvaluationContext,
|
|
85
|
+
PolicyBundle,
|
|
86
|
+
PolicyEvaluator,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
bundle = PolicyBundle.model_validate({
|
|
90
|
+
"bundle_id": "edu-gate",
|
|
91
|
+
"policies": [{
|
|
92
|
+
"id": "writes-require-admin",
|
|
93
|
+
"default_effect": "deny",
|
|
94
|
+
"rules": [
|
|
95
|
+
{
|
|
96
|
+
"id": "admin-writes",
|
|
97
|
+
"effect": "allow",
|
|
98
|
+
"when": {
|
|
99
|
+
"kind": "all_of",
|
|
100
|
+
"matchers": [
|
|
101
|
+
{"kind": "in", "field": "action", "value": ["create", "update", "delete"]},
|
|
102
|
+
{"kind": "eq", "field": "subject.role", "value": "admin"},
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
}],
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
ctx = EvaluationContext(
|
|
111
|
+
subject={"id": "u-42", "role": "admin"},
|
|
112
|
+
action="update",
|
|
113
|
+
resource={"id": "doc-7"},
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
result = PolicyEvaluator().evaluate(bundle, ctx)
|
|
117
|
+
print(result.decision.kind) # "allow"
|
|
118
|
+
print(result.decision.matched_rule_id) # "admin-writes"
|
|
119
|
+
print(result.decision.reason) # "matched rule 'admin-writes'"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`result.policy_decisions` carries every per-policy outcome. Callers that need durable audit records must persist them in their own trusted system.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Bundle DSL
|
|
127
|
+
|
|
128
|
+
A bundle is a small recursive structure. Matchers compose; rules ordered.
|
|
129
|
+
|
|
130
|
+
### Field matchers
|
|
131
|
+
|
|
132
|
+
| Kind | Notes |
|
|
133
|
+
| --------------- | --- |
|
|
134
|
+
| `eq` / `ne` | Strict equality. |
|
|
135
|
+
| `gt` / `gte` / `lt` / `lte` | Comparison; returns `false` on incompatible types (won't raise). |
|
|
136
|
+
| `in` / `not_in` | `value` must be a list. |
|
|
137
|
+
| `contains` | Works against strings, lists, sets, dicts. |
|
|
138
|
+
| `exists` / `missing` | No `value`. Operates against the dotted-path resolver. |
|
|
139
|
+
| `regex` | Patterns have a 256-character limit; input is limited to 4,096 characters and each match has a 20 ms timeout. Compiled patterns are cached with a 128-entry cap. |
|
|
140
|
+
| `starts_with` / `ends_with` | String-only. |
|
|
141
|
+
|
|
142
|
+
### Composite matchers
|
|
143
|
+
|
|
144
|
+
| Kind | Children | Truth |
|
|
145
|
+
| -------- | -------- | --- |
|
|
146
|
+
| `all_of` | `matchers: [...]` | All children true. |
|
|
147
|
+
| `any_of` | `matchers: [...]` | At least one child true. |
|
|
148
|
+
| `not` | `matcher: {...}` | Inverts the child. |
|
|
149
|
+
| `always` | — | Always true. Useful as a final catch-all. |
|
|
150
|
+
|
|
151
|
+
### Dotted paths
|
|
152
|
+
|
|
153
|
+
The resolver looks at the merged context (`data` + `subject` + `action` + `resource`):
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
subject.role
|
|
157
|
+
resource.tags.0 # list index
|
|
158
|
+
conditions_satisfied.dpa-signed
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Missing segments produce a `_MISSING` sentinel — `exists` / `missing` matchers see it; every other matcher returns `false`. Escape a literal dot or backslash in a key with a backslash, for example `conditions_satisfied.risk\.review`.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## FastAPI surface
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
pip install "policy-as-code-engine[api]"
|
|
169
|
+
python -m policy_as_code_engine # binds 127.0.0.1:8089 by default
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Before starting, configure **distinct** random values of at least 32 characters for `POLICY_ENGINE_ADMIN_TOKEN` and `POLICY_ENGINE_EVALUATE_TOKEN` through your secret manager. The service returns 503 on protected routes when either is absent, short, or reused. Send `Authorization: Bearer <token>` over TLS. The admin token can register, list, inspect, and evaluate bundles; the evaluator token can only call `POST /bundles/{bundle_id}/evaluate`. `/` and `/healthz` remain public. Restarting loses all bundles and condition assertions.
|
|
173
|
+
|
|
174
|
+
For positive Decision Cards, configure `POLICY_ENGINE_BUYER_KEYS_JSON` as a JSON map of trusted buyer ID to expected key URL to base64-encoded 32-byte Ed25519 public key. Pin these values from an independent buyer authority process; neither a card's `buyer.id` nor its attestation `key_url` establishes trust by itself. No real key or token is included in this README.
|
|
175
|
+
|
|
176
|
+
| Method | Path | What it does |
|
|
177
|
+
| --- | --- | --- |
|
|
178
|
+
| GET | `/healthz` | Liveness probe. |
|
|
179
|
+
| GET | `/` | Service info. |
|
|
180
|
+
| POST | `/bundles` | Register a `PolicyBundle` in memory. |
|
|
181
|
+
| GET | `/bundles` | List registered bundle IDs. |
|
|
182
|
+
| GET | `/bundles/{bundle_id}` | Inspect a registered bundle. |
|
|
183
|
+
| POST | `/bundles/{bundle_id}/evaluate` | Evaluate a stored bundle against an `EvaluationContext`. |
|
|
184
|
+
| POST | `/evaluate` | One-shot. Bundle + context in, decision out. |
|
|
185
|
+
| POST | `/bundles/from-decision-card` | Verify and register a scoped v0.1 Decision Card bundle. Admin only. |
|
|
186
|
+
| PUT | `/bundles/{bundle_id}/conditions/{condition_id}` | Admin assertion of a condition state with expiry within seven days. |
|
|
187
|
+
|
|
188
|
+
POST and PUT bodies are capped at 128 KiB. Registration rejects a duplicate bundle ID with 409. Add an edge rate limit, TLS, and network access controls before exposing the ASGI app; the built-in bearer check has no per-client rate limit or tenant isolation.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## The cross-ecosystem hook
|
|
193
|
+
|
|
194
|
+
An AI Procurement Decision Card records a buyer's posture toward a vendor. The current procurement API returns `{ "draft": ..., "documents_fetched": ..., "fetch_errors": ..., "document_hashes": ..., "suggested_status": ... }`. Its `draft` remains pending; `suggested_status` is advisory. A buyer review/signing system must produce a final card and a v2 `hash-attestation-rs` attestation before positive HTTP registration. This repo does not supply that approval workflow.
|
|
195
|
+
|
|
196
|
+
The endpoint accepts an envelope with `card`, `attestation`, and `allowed_actions`. The card must be v0.1. For approved statuses, it must contain `buyer.id`, `subject.vendor_id`, and a timezone-qualified `decision.effective_until`. The HTTP bridge accepts only `allowed_actions: ["use"]` until the buyer card has a machine-readable signed operation scope. This local choice does not mean the buyer signed action authorization. The bridge rejects duplicate JSON object keys, non-finite numbers, unsupported card versions, and unknown top-level or relevant nested fields. It does not validate the full upstream schema.
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
curl -X POST http://localhost:8089/bundles/from-decision-card \
|
|
200
|
+
-H "Authorization: Bearer $POLICY_ENGINE_ADMIN_TOKEN" \
|
|
201
|
+
-H 'Content-Type: application/json' \
|
|
202
|
+
-d @approved-card-registration.json
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The JSON envelope has this shape; values shown in angle brackets are placeholders:
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"card": { "decision_card_version": "0.1", "decision_id": "<final-card-id>", "buyer": { "id": "<pinned-buyer-id>" }, "decision": { "status": "approved", "effective_until": "<RFC3339-time>" }, "subject": { "vendor_name": "<name>", "vendor_id": "<vendor-id>" } },
|
|
210
|
+
"attestation": { "algorithm": "ed25519", "hash_profile": "jcs-rfc8785-v1", "signed_hash": "<sha256:...>", "signature": "<base64-signature>", "key_url": "<pinned-key-url>", "signed_at": "<RFC3339-time>" },
|
|
211
|
+
"allowed_actions": ["use"]
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The example omits other card fields; the actual final card and attestation must match exactly. The verifier checks RFC 8785 canonical hash and the Rust v2 domain-separated signature. Legacy signatures are rejected. A malformed or untrusted positive card cannot register. A trusted signature proves possession of the pinned key, not the signer's legal authority, the accuracy of the buyer's review, or that the card has not since been revoked.
|
|
216
|
+
|
|
217
|
+
Mapping:
|
|
218
|
+
|
|
219
|
+
| Decision Card status | Resulting bundle |
|
|
220
|
+
| --- | --- |
|
|
221
|
+
| `approved` | One policy that allows only the configured action(s) on `resource.vendor_id` matching the signed card's `subject.vendor_id`; otherwise denies. |
|
|
222
|
+
| `rejected` · `rejected-with-remediation` · `withdrawn` · `expired` · `pending` | Single `deny-all` policy (fail safe). |
|
|
223
|
+
| `approved-with-conditions` | One policy per condition. Every condition must have a current admin assertion of boolean `true` and the action/vendor scope must match; otherwise denies. Evaluation callers cannot supply `conditions_satisfied` to the HTTP API. |
|
|
224
|
+
|
|
225
|
+
The admin must derive condition assertions from evidence outside this service, then PUT `{ "satisfied": true, "valid_until": "<RFC3339-time-within-seven-days>" }` to the condition endpoint. An unset or expired assertion denies. Assertions and bundles disappear on restart. The bridge rejects duplicate condition IDs and preserves literal dots and backslashes in IDs. The evaluator checks card effective times against its own UTC clock on every evaluation; invalid or offset-free values are rejected at conversion.
|
|
226
|
+
|
|
227
|
+
The Python library requires `policy_bundle_from_decision_card(card, allowed_actions=["use"], attestation=attestation, trusted_key_url=key_url, trusted_public_key=public_key)` for positive cards. It verifies the v2 signature, but the calling process must map `buyer.id` to an independently trusted key and provide verified condition signals. The HTTP adapter handles the pinned-key mapping and replaces caller condition data with admin assertions. Decision Card conversion permits only `use`; applications author other operation rules separately.
|
|
228
|
+
|
|
229
|
+
Evaluate converted cards with `PolicyEvaluator.evaluate(bundle, context)` or the stored HTTP evaluation route. `evaluate_policy(policy, context)` deliberately returns deny for card-derived policies because it has no bundle time window or scope metadata.
|
|
230
|
+
|
|
231
|
+
### Production boundary
|
|
232
|
+
|
|
233
|
+
The API is a local/reference integration. Bearer roles, buyer-key checks, vendor/action binding, condition assertions, and request bounds protect its basic boundary, but it has no durable bundle/condition store, revocation feed, key-rotation workflow, tenant isolation, or built-in rate limiting. `decision.scope` remains free text and is **not** parsed into the action mapping. The evaluating service must derive `action` and `resource.vendor_id` from its own authenticated request, select the right bundle, and combine the result with its own subject/resource authorization. A card approval alone is not permission to run every operation. A buyer withdrawal after registration is not discovered automatically; operators must stop using the bundle and update the source of truth. Do not treat this reference API as standalone production authorization until those controls are designed and tested for the deployment.
|
|
234
|
+
|
|
235
|
+
When `AUDIT_STREAM_URL` is configured, registration, condition assertion, and allow/deny events are posted synchronously and best effort. Free-text reasons, condition descriptions, and bundle sources are excluded from these outbound events; IDs may still be sensitive in some deployments. Failed delivery is not retried or durably queued and can add up to the configured timeout to a request. Use a trusted local audit path and a separate persistence design when audit completeness matters.
|
|
236
|
+
|
|
237
|
+
### Migrating from 0.1.1 to 0.2.0
|
|
238
|
+
|
|
239
|
+
This release intentionally breaks the permissive Decision Card path. Set separate admin and evaluator tokens before using protected HTTP routes. Wrap bridge requests as `{ "card": ..., "attestation": ..., "allowed_actions": ["use"] }`; an approved card needs a v2 Rust-compatible signature and an independently pinned buyer key. Add `buyer.id`, `subject.vendor_id`, and a timezone-qualified `decision.effective_until` to positive cards. Library callers must pass the attestation and trusted buyer key explicitly. The generated bundle now defaults to deny outside its vendor/action scope, and the HTTP evaluator rejects caller-supplied condition booleans. Admins assert condition state separately with a maximum seven-day validity. Bundle IDs cannot overwrite existing registrations; restart or create a new decision ID to replace one. Existing stored bundles are in memory only and are erased on restart, so migration requires explicit re-registration.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## CLI
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
python -m policy_as_code_engine eval examples/example-bundle.yaml examples/example-context.json
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Prints the full `EvaluationResult` as JSON. Exits non-zero on `deny`.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## How decisions combine
|
|
254
|
+
|
|
255
|
+
Inside a single policy: **first matching rule wins**, otherwise `default_effect`.
|
|
256
|
+
|
|
257
|
+
Across the bundle:
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
deny -> deny (any policy denies => bundle denies)
|
|
261
|
+
allow -> allow (otherwise, any allow => bundle allows)
|
|
262
|
+
neither -> not_applicable
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Per-policy decisions are always returned — useful for "we denied because of policy B, but A would have allowed" audit narratives.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Tests
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
pip install -e ".[dev]"
|
|
273
|
+
ruff check src tests scripts && ruff format --check src tests scripts
|
|
274
|
+
mypy src scripts
|
|
275
|
+
pytest -v
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
CI matrix runs Python 3.11 / 3.12 / 3.13.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Related in this ecosystem
|
|
283
|
+
|
|
284
|
+
- **[procurement-decision-api](https://github.com/mizcausevic-dev/procurement-decision-api)** — drafts Decision Cards for buyer review; it does not issue signed approvals.
|
|
285
|
+
- **[ai-procurement-decision-spec](https://github.com/mizcausevic-dev/ai-procurement-decision-spec)** — the v0.1 schema.
|
|
286
|
+
- **[slo-budget-tracker](https://github.com/mizcausevic-dev/slo-budget-tracker)** — error-budget tracker that you can wire into the same FastAPI app.
|
|
287
|
+
- **[reliability-toolkit-rs](https://github.com/mizcausevic-dev/reliability-toolkit-rs)** — Rust async reliability primitives.
|
|
288
|
+
- More at [kineticgain.com](https://kineticgain.com/).
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## License
|
|
293
|
+
|
|
294
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,247 @@
|
|
|
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. The HTTP bridge converts an operator-trusted, attested v0.1 Decision Card into a vendor-scoped `use` bundle after verifying its v2 signature against a pinned key. The operator must verify buyer authority and condition evidence separately.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
The engine keeps policy rules in JSON/YAML and returns the reason for each decision. Its Decision Card bridge is designed for cards that have completed buyer review and an independent key-trust process. It:
|
|
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. The library does not persist an audit log; optional outbound audit events are best effort.
|
|
17
|
+
3. **Bridges to the Kinetic Gain Protocol Suite.** Positive card conversion verifies a buyer-key attestation; the caller must pin the buyer key independently. The HTTP adapter also keeps condition assertions behind the admin credential, with a seven-day maximum validity.
|
|
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`, `httpx`, `cryptography`, `rfc8785`, and `regex`. The last three provide Ed25519 verification, cross-language canonical JSON, and bounded regex matching.
|
|
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. Callers that need durable audit records must persist them in their own trusted system.
|
|
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` | Patterns have a 256-character limit; input is limited to 4,096 characters and each match has a 20 ms timeout. Compiled patterns are cached with a 128-entry cap. |
|
|
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
|
+
conditions_satisfied.dpa-signed
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Missing segments produce a `_MISSING` sentinel — `exists` / `missing` matchers see it; every other matcher returns `false`. Escape a literal dot or backslash in a key with a backslash, for example `conditions_satisfied.risk\.review`.
|
|
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 127.0.0.1:8089 by default
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Before starting, configure **distinct** random values of at least 32 characters for `POLICY_ENGINE_ADMIN_TOKEN` and `POLICY_ENGINE_EVALUATE_TOKEN` through your secret manager. The service returns 503 on protected routes when either is absent, short, or reused. Send `Authorization: Bearer <token>` over TLS. The admin token can register, list, inspect, and evaluate bundles; the evaluator token can only call `POST /bundles/{bundle_id}/evaluate`. `/` and `/healthz` remain public. Restarting loses all bundles and condition assertions.
|
|
126
|
+
|
|
127
|
+
For positive Decision Cards, configure `POLICY_ENGINE_BUYER_KEYS_JSON` as a JSON map of trusted buyer ID to expected key URL to base64-encoded 32-byte Ed25519 public key. Pin these values from an independent buyer authority process; neither a card's `buyer.id` nor its attestation `key_url` establishes trust by itself. No real key or token is included in this README.
|
|
128
|
+
|
|
129
|
+
| Method | Path | What it does |
|
|
130
|
+
| --- | --- | --- |
|
|
131
|
+
| GET | `/healthz` | Liveness probe. |
|
|
132
|
+
| GET | `/` | Service info. |
|
|
133
|
+
| POST | `/bundles` | Register a `PolicyBundle` in memory. |
|
|
134
|
+
| GET | `/bundles` | List registered bundle IDs. |
|
|
135
|
+
| GET | `/bundles/{bundle_id}` | Inspect a registered bundle. |
|
|
136
|
+
| POST | `/bundles/{bundle_id}/evaluate` | Evaluate a stored bundle against an `EvaluationContext`. |
|
|
137
|
+
| POST | `/evaluate` | One-shot. Bundle + context in, decision out. |
|
|
138
|
+
| POST | `/bundles/from-decision-card` | Verify and register a scoped v0.1 Decision Card bundle. Admin only. |
|
|
139
|
+
| PUT | `/bundles/{bundle_id}/conditions/{condition_id}` | Admin assertion of a condition state with expiry within seven days. |
|
|
140
|
+
|
|
141
|
+
POST and PUT bodies are capped at 128 KiB. Registration rejects a duplicate bundle ID with 409. Add an edge rate limit, TLS, and network access controls before exposing the ASGI app; the built-in bearer check has no per-client rate limit or tenant isolation.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## The cross-ecosystem hook
|
|
146
|
+
|
|
147
|
+
An AI Procurement Decision Card records a buyer's posture toward a vendor. The current procurement API returns `{ "draft": ..., "documents_fetched": ..., "fetch_errors": ..., "document_hashes": ..., "suggested_status": ... }`. Its `draft` remains pending; `suggested_status` is advisory. A buyer review/signing system must produce a final card and a v2 `hash-attestation-rs` attestation before positive HTTP registration. This repo does not supply that approval workflow.
|
|
148
|
+
|
|
149
|
+
The endpoint accepts an envelope with `card`, `attestation`, and `allowed_actions`. The card must be v0.1. For approved statuses, it must contain `buyer.id`, `subject.vendor_id`, and a timezone-qualified `decision.effective_until`. The HTTP bridge accepts only `allowed_actions: ["use"]` until the buyer card has a machine-readable signed operation scope. This local choice does not mean the buyer signed action authorization. The bridge rejects duplicate JSON object keys, non-finite numbers, unsupported card versions, and unknown top-level or relevant nested fields. It does not validate the full upstream schema.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
curl -X POST http://localhost:8089/bundles/from-decision-card \
|
|
153
|
+
-H "Authorization: Bearer $POLICY_ENGINE_ADMIN_TOKEN" \
|
|
154
|
+
-H 'Content-Type: application/json' \
|
|
155
|
+
-d @approved-card-registration.json
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The JSON envelope has this shape; values shown in angle brackets are placeholders:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"card": { "decision_card_version": "0.1", "decision_id": "<final-card-id>", "buyer": { "id": "<pinned-buyer-id>" }, "decision": { "status": "approved", "effective_until": "<RFC3339-time>" }, "subject": { "vendor_name": "<name>", "vendor_id": "<vendor-id>" } },
|
|
163
|
+
"attestation": { "algorithm": "ed25519", "hash_profile": "jcs-rfc8785-v1", "signed_hash": "<sha256:...>", "signature": "<base64-signature>", "key_url": "<pinned-key-url>", "signed_at": "<RFC3339-time>" },
|
|
164
|
+
"allowed_actions": ["use"]
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The example omits other card fields; the actual final card and attestation must match exactly. The verifier checks RFC 8785 canonical hash and the Rust v2 domain-separated signature. Legacy signatures are rejected. A malformed or untrusted positive card cannot register. A trusted signature proves possession of the pinned key, not the signer's legal authority, the accuracy of the buyer's review, or that the card has not since been revoked.
|
|
169
|
+
|
|
170
|
+
Mapping:
|
|
171
|
+
|
|
172
|
+
| Decision Card status | Resulting bundle |
|
|
173
|
+
| --- | --- |
|
|
174
|
+
| `approved` | One policy that allows only the configured action(s) on `resource.vendor_id` matching the signed card's `subject.vendor_id`; otherwise denies. |
|
|
175
|
+
| `rejected` · `rejected-with-remediation` · `withdrawn` · `expired` · `pending` | Single `deny-all` policy (fail safe). |
|
|
176
|
+
| `approved-with-conditions` | One policy per condition. Every condition must have a current admin assertion of boolean `true` and the action/vendor scope must match; otherwise denies. Evaluation callers cannot supply `conditions_satisfied` to the HTTP API. |
|
|
177
|
+
|
|
178
|
+
The admin must derive condition assertions from evidence outside this service, then PUT `{ "satisfied": true, "valid_until": "<RFC3339-time-within-seven-days>" }` to the condition endpoint. An unset or expired assertion denies. Assertions and bundles disappear on restart. The bridge rejects duplicate condition IDs and preserves literal dots and backslashes in IDs. The evaluator checks card effective times against its own UTC clock on every evaluation; invalid or offset-free values are rejected at conversion.
|
|
179
|
+
|
|
180
|
+
The Python library requires `policy_bundle_from_decision_card(card, allowed_actions=["use"], attestation=attestation, trusted_key_url=key_url, trusted_public_key=public_key)` for positive cards. It verifies the v2 signature, but the calling process must map `buyer.id` to an independently trusted key and provide verified condition signals. The HTTP adapter handles the pinned-key mapping and replaces caller condition data with admin assertions. Decision Card conversion permits only `use`; applications author other operation rules separately.
|
|
181
|
+
|
|
182
|
+
Evaluate converted cards with `PolicyEvaluator.evaluate(bundle, context)` or the stored HTTP evaluation route. `evaluate_policy(policy, context)` deliberately returns deny for card-derived policies because it has no bundle time window or scope metadata.
|
|
183
|
+
|
|
184
|
+
### Production boundary
|
|
185
|
+
|
|
186
|
+
The API is a local/reference integration. Bearer roles, buyer-key checks, vendor/action binding, condition assertions, and request bounds protect its basic boundary, but it has no durable bundle/condition store, revocation feed, key-rotation workflow, tenant isolation, or built-in rate limiting. `decision.scope` remains free text and is **not** parsed into the action mapping. The evaluating service must derive `action` and `resource.vendor_id` from its own authenticated request, select the right bundle, and combine the result with its own subject/resource authorization. A card approval alone is not permission to run every operation. A buyer withdrawal after registration is not discovered automatically; operators must stop using the bundle and update the source of truth. Do not treat this reference API as standalone production authorization until those controls are designed and tested for the deployment.
|
|
187
|
+
|
|
188
|
+
When `AUDIT_STREAM_URL` is configured, registration, condition assertion, and allow/deny events are posted synchronously and best effort. Free-text reasons, condition descriptions, and bundle sources are excluded from these outbound events; IDs may still be sensitive in some deployments. Failed delivery is not retried or durably queued and can add up to the configured timeout to a request. Use a trusted local audit path and a separate persistence design when audit completeness matters.
|
|
189
|
+
|
|
190
|
+
### Migrating from 0.1.1 to 0.2.0
|
|
191
|
+
|
|
192
|
+
This release intentionally breaks the permissive Decision Card path. Set separate admin and evaluator tokens before using protected HTTP routes. Wrap bridge requests as `{ "card": ..., "attestation": ..., "allowed_actions": ["use"] }`; an approved card needs a v2 Rust-compatible signature and an independently pinned buyer key. Add `buyer.id`, `subject.vendor_id`, and a timezone-qualified `decision.effective_until` to positive cards. Library callers must pass the attestation and trusted buyer key explicitly. The generated bundle now defaults to deny outside its vendor/action scope, and the HTTP evaluator rejects caller-supplied condition booleans. Admins assert condition state separately with a maximum seven-day validity. Bundle IDs cannot overwrite existing registrations; restart or create a new decision ID to replace one. Existing stored bundles are in memory only and are erased on restart, so migration requires explicit re-registration.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## CLI
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
python -m policy_as_code_engine eval examples/example-bundle.yaml examples/example-context.json
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Prints the full `EvaluationResult` as JSON. Exits non-zero on `deny`.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## How decisions combine
|
|
207
|
+
|
|
208
|
+
Inside a single policy: **first matching rule wins**, otherwise `default_effect`.
|
|
209
|
+
|
|
210
|
+
Across the bundle:
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
deny -> deny (any policy denies => bundle denies)
|
|
214
|
+
allow -> allow (otherwise, any allow => bundle allows)
|
|
215
|
+
neither -> not_applicable
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Per-policy decisions are always returned — useful for "we denied because of policy B, but A would have allowed" audit narratives.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Tests
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
pip install -e ".[dev]"
|
|
226
|
+
ruff check src tests scripts && ruff format --check src tests scripts
|
|
227
|
+
mypy src scripts
|
|
228
|
+
pytest -v
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
CI matrix runs Python 3.11 / 3.12 / 3.13.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Related in this ecosystem
|
|
236
|
+
|
|
237
|
+
- **[procurement-decision-api](https://github.com/mizcausevic-dev/procurement-decision-api)** — drafts Decision Cards for buyer review; it does not issue signed approvals.
|
|
238
|
+
- **[ai-procurement-decision-spec](https://github.com/mizcausevic-dev/ai-procurement-decision-spec)** — the v0.1 schema.
|
|
239
|
+
- **[slo-budget-tracker](https://github.com/mizcausevic-dev/slo-budget-tracker)** — error-budget tracker that you can wire into the same FastAPI app.
|
|
240
|
+
- **[reliability-toolkit-rs](https://github.com/mizcausevic-dev/reliability-toolkit-rs)** — Rust async reliability primitives.
|
|
241
|
+
- More at [kineticgain.com](https://kineticgain.com/).
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## License
|
|
246
|
+
|
|
247
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["hatchling
|
|
2
|
+
requires = ["hatchling==1.32.4"]
|
|
3
3
|
build-backend = "hatchling.build"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "policy-as-code-engine"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "Declarative policy
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Declarative policy evaluator with Decision Card field conversion and optional best-effort audit events."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
11
11
|
requires-python = ">=3.11"
|
|
@@ -31,9 +31,12 @@ classifiers = [
|
|
|
31
31
|
"Typing :: Typed",
|
|
32
32
|
]
|
|
33
33
|
dependencies = [
|
|
34
|
+
"cryptography>=50.0.2,<51",
|
|
34
35
|
"httpx>=0.27",
|
|
35
36
|
"pydantic>=2.7",
|
|
36
37
|
"PyYAML>=6.0",
|
|
38
|
+
"regex>=2024.11.6,<2027",
|
|
39
|
+
"rfc8785>=0.1.4,<0.2",
|
|
37
40
|
]
|
|
38
41
|
|
|
39
42
|
[project.optional-dependencies]
|
|
@@ -50,6 +53,7 @@ dev = [
|
|
|
50
53
|
"ruff>=0.6",
|
|
51
54
|
"mypy>=1.11",
|
|
52
55
|
"types-PyYAML>=6.0",
|
|
56
|
+
"types-regex>=2024.11.6",
|
|
53
57
|
]
|
|
54
58
|
|
|
55
59
|
[project.scripts]
|
|
@@ -65,6 +69,9 @@ Issues = "https://github.com/mizcausevic-dev/policy-as-code-engine/issues
|
|
|
65
69
|
[tool.hatch.build.targets.wheel]
|
|
66
70
|
packages = ["src/policy_as_code_engine"]
|
|
67
71
|
|
|
72
|
+
[tool.hatch.build.targets.sdist]
|
|
73
|
+
only-include = ["src", "tests", "examples", "LICENSE", "README.md"]
|
|
74
|
+
|
|
68
75
|
[tool.pytest.ini_options]
|
|
69
76
|
testpaths = ["tests"]
|
|
70
77
|
asyncio_mode = "auto"
|
{policy_as_code_engine-0.1.1 → policy_as_code_engine-0.2.0}/src/policy_as_code_engine/__main__.py
RENAMED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Entry point. Either run the HTTP API or evaluate a bundle from the CLI.
|
|
3
3
|
|
|
4
4
|
# API
|
|
5
|
-
python -m policy_as_code_engine # binds
|
|
5
|
+
python -m policy_as_code_engine # binds 127.0.0.1:8089
|
|
6
6
|
|
|
7
7
|
# CLI eval
|
|
8
8
|
python -m policy_as_code_engine eval bundle.yaml ctx.json
|
|
@@ -24,7 +24,7 @@ def _serve() -> None:
|
|
|
24
24
|
import uvicorn
|
|
25
25
|
|
|
26
26
|
port = int(os.environ.get("PORT", "8089"))
|
|
27
|
-
host = os.environ.get("HOST", "
|
|
27
|
+
host = os.environ.get("HOST", "127.0.0.1")
|
|
28
28
|
uvicorn.run("policy_as_code_engine.app:app", host=host, port=port, log_level="info")
|
|
29
29
|
|
|
30
30
|
|