policy-as-code-engine 0.2.0__tar.gz → 0.2.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.2.0 → policy_as_code_engine-0.2.1}/PKG-INFO +10 -2
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/README.md +9 -1
- policy_as_code_engine-0.2.1/docs/SYNTHETIC_PILOT.md +115 -0
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/pyproject.toml +2 -2
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/src/policy_as_code_engine/__init__.py +1 -1
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/src/policy_as_code_engine/audit_stream.py +58 -7
- policy_as_code_engine-0.2.1/src/policy_as_code_engine/pilot_app.py +685 -0
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/tests/test_app.py +4 -2
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/tests/test_audit_stream.py +82 -7
- policy_as_code_engine-0.2.1/tests/test_pilot_app.py +419 -0
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/.gitignore +0 -0
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/LICENSE +0 -0
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/examples/example-bundle.yaml +0 -0
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/examples/example-context.json +0 -0
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/src/policy_as_code_engine/__main__.py +0 -0
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/src/policy_as_code_engine/app.py +0 -0
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/src/policy_as_code_engine/card_attestation.py +0 -0
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/src/policy_as_code_engine/evaluator.py +0 -0
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/src/policy_as_code_engine/from_decision_card.py +0 -0
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/src/policy_as_code_engine/loader.py +0 -0
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/src/policy_as_code_engine/models.py +0 -0
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/src/policy_as_code_engine/py.typed +0 -0
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/tests/__init__.py +0 -0
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/tests/test_card_attestation.py +0 -0
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/tests/test_evaluator.py +0 -0
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/tests/test_from_decision_card.py +0 -0
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/tests/test_loader.py +0 -0
- {policy_as_code_engine-0.2.0 → policy_as_code_engine-0.2.1}/tests/test_models.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: policy-as-code-engine
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Declarative policy evaluator with Decision Card field conversion and optional best-effort audit events.
|
|
5
5
|
Project-URL: Homepage, https://github.com/mizcausevic-dev/policy-as-code-engine
|
|
6
6
|
Project-URL: Repository, https://github.com/mizcausevic-dev/policy-as-code-engine
|
|
@@ -232,7 +232,15 @@ Evaluate converted cards with `PolicyEvaluator.evaluate(bundle, context)` or the
|
|
|
232
232
|
|
|
233
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
234
|
|
|
235
|
-
When `AUDIT_STREAM_URL` is configured,
|
|
235
|
+
When `AUDIT_STREAM_URL` is configured with the private sink base URL or exact `/events` endpoint, also set `AUDIT_STREAM_TOKEN` to the sink's separate bearer credential (at least 32 visible ASCII characters) from a secret store. The URL requires HTTPS except for numeric loopback HTTP addresses. Registration, condition assertion, and allow/deny events are posted synchronously and best effort. Invalid configuration prevents outbound delivery and logs a failure; HTTP errors are logged without the URL or token. 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
|
+
### Opt-in synthetic pilot
|
|
238
|
+
|
|
239
|
+
The v0.2.1 source tree packages a separate `policy_as_code_engine.pilot_app:app` for **local synthetic drills**. It uses tenant-scoped, short-lived Ed25519 assertions; a local SQLite file for buyer-key/card/condition state and committed minimal receipts; per-evaluation card/key revocation checks; and a deny-all default. It has no generic bundle or one-shot evaluation route. The main reference API above does not invoke this pilot surface, and the published v0.2.0 package does not contain it. See [the synthetic pilot guide](docs/SYNTHETIC_PILOT.md) for configuration, tests, and limitations. A local SQLite file, fictional issuer, and loopback guard do not clear hosted customer-use gates.
|
|
240
|
+
|
|
241
|
+
### v0.2.1 changes
|
|
242
|
+
|
|
243
|
+
This version adds authenticated, redirect-safe, best-effort audit delivery and packages the opt-in synthetic pilot module. If you already set `AUDIT_STREAM_URL`, configure the sink to require a separate bearer credential and set `AUDIT_STREAM_TOKEN` before upgrading. Without a valid token, audit delivery fails and is logged, while policy evaluation continues because delivery is best effort. This version does not add hosted authorization, audit completeness, or a revocation feed to the reference API. A source version string alone does not establish PyPI availability; check the tagged workflow and published artifacts before making a release claim.
|
|
236
244
|
|
|
237
245
|
### Migrating from 0.1.1 to 0.2.0
|
|
238
246
|
|
|
@@ -185,7 +185,15 @@ Evaluate converted cards with `PolicyEvaluator.evaluate(bundle, context)` or the
|
|
|
185
185
|
|
|
186
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
187
|
|
|
188
|
-
When `AUDIT_STREAM_URL` is configured,
|
|
188
|
+
When `AUDIT_STREAM_URL` is configured with the private sink base URL or exact `/events` endpoint, also set `AUDIT_STREAM_TOKEN` to the sink's separate bearer credential (at least 32 visible ASCII characters) from a secret store. The URL requires HTTPS except for numeric loopback HTTP addresses. Registration, condition assertion, and allow/deny events are posted synchronously and best effort. Invalid configuration prevents outbound delivery and logs a failure; HTTP errors are logged without the URL or token. 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
|
+
### Opt-in synthetic pilot
|
|
191
|
+
|
|
192
|
+
The v0.2.1 source tree packages a separate `policy_as_code_engine.pilot_app:app` for **local synthetic drills**. It uses tenant-scoped, short-lived Ed25519 assertions; a local SQLite file for buyer-key/card/condition state and committed minimal receipts; per-evaluation card/key revocation checks; and a deny-all default. It has no generic bundle or one-shot evaluation route. The main reference API above does not invoke this pilot surface, and the published v0.2.0 package does not contain it. See [the synthetic pilot guide](docs/SYNTHETIC_PILOT.md) for configuration, tests, and limitations. A local SQLite file, fictional issuer, and loopback guard do not clear hosted customer-use gates.
|
|
193
|
+
|
|
194
|
+
### v0.2.1 changes
|
|
195
|
+
|
|
196
|
+
This version adds authenticated, redirect-safe, best-effort audit delivery and packages the opt-in synthetic pilot module. If you already set `AUDIT_STREAM_URL`, configure the sink to require a separate bearer credential and set `AUDIT_STREAM_TOKEN` before upgrading. Without a valid token, audit delivery fails and is logged, while policy evaluation continues because delivery is best effort. This version does not add hosted authorization, audit completeness, or a revocation feed to the reference API. A source version string alone does not establish PyPI availability; check the tagged workflow and published artifacts before making a release claim.
|
|
189
197
|
|
|
190
198
|
### Migrating from 0.1.1 to 0.2.0
|
|
191
199
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Local synthetic policy pilot
|
|
2
|
+
|
|
3
|
+
The v0.2.1 source tree packages `policy_as_code_engine.pilot_app:app`
|
|
4
|
+
as a separate, opt-in FastAPI surface. The published v0.2.0 package does not
|
|
5
|
+
contain it, and the main reference HTTP API does not invoke it. **Use fictional
|
|
6
|
+
buyers, vendors, cards, and assertions only.**
|
|
7
|
+
The pilot is not a customer authorization service or a hosted deployment.
|
|
8
|
+
|
|
9
|
+
## What it proves locally
|
|
10
|
+
|
|
11
|
+
- A short-lived Ed25519 assertion must name the configured issuer and audience,
|
|
12
|
+
tenant, subject, and `admin` or `evaluate` role. Evaluation assertions must
|
|
13
|
+
also name the exact `use` action and vendor. The service builds the policy
|
|
14
|
+
context from that signed assertion, not from an evaluation request body.
|
|
15
|
+
- Buyer public keys are pinned to a tenant, buyer ID, key ID, and URL by a
|
|
16
|
+
pilot administrator. A positive v0.1 Decision Card needs its exact v2
|
|
17
|
+
attestation and a currently active key. The URL selects the enrolled key;
|
|
18
|
+
it does not prove the buyer's organizational authority.
|
|
19
|
+
- SQLite holds keys, derived bundles, condition assertions, and minimal audit
|
|
20
|
+
receipts on one local disk. Each state mutation and its receipt commit in one
|
|
21
|
+
transaction. Evaluation inserts a receipt before returning an `allow`.
|
|
22
|
+
Missing or unwritable state returns 503. A card or buyer-key revocation is
|
|
23
|
+
checked again on the next evaluation, including after process restart.
|
|
24
|
+
- Positive results require **both** `POLICY_PILOT_ALLOW_SYNTHETIC=1` and
|
|
25
|
+
`POLICY_PILOT_DENY_ALL=0`. The default is deny-all. The app's own entry point
|
|
26
|
+
binds `127.0.0.1:8090`, and a non-loopback caller is rejected. Requests are
|
|
27
|
+
capped at 128 KiB, 32 JSON nesting levels, and five seconds; responses
|
|
28
|
+
request `Cache-Control: no-store`.
|
|
29
|
+
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
Install the repository with `pip install -e ".[dev]"`, then set these values
|
|
33
|
+
in the local process environment. Keep private signing keys outside the repo.
|
|
34
|
+
The service receives **public keys only**; an independent trusted caller mints
|
|
35
|
+
the signed assertions. Do not use the fictional keys in `tests/` for any real
|
|
36
|
+
identity or buyer enrollment.
|
|
37
|
+
|
|
38
|
+
| Variable | Purpose |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| `POLICY_PILOT_DB_PATH` | Absolute path to a local SQLite file in an existing private directory. It is created on startup. Do not place it on Cloud Run's ephemeral filesystem for a hosted claim. |
|
|
41
|
+
| `POLICY_PILOT_ISSUER` | Exact synthetic assertion issuer string. |
|
|
42
|
+
| `POLICY_PILOT_AUDIENCE` | Exact audience of this pilot service. |
|
|
43
|
+
| `POLICY_PILOT_ISSUER_PUBLIC_KEY_B64` | Base64 encoding of the issuer's 32-byte Ed25519 public key. |
|
|
44
|
+
| `POLICY_PILOT_ALLOW_SYNTHETIC` | Must equal `1` before any `allow` is possible. |
|
|
45
|
+
| `POLICY_PILOT_DENY_ALL` | Defaults to `1`; must equal `0` for a synthetic `allow`. Set to `1` first in a local rollback drill. |
|
|
46
|
+
|
|
47
|
+
The compact bearer assertion has a fixed `{"alg":"EdDSA","typ":"JWT"}`
|
|
48
|
+
header and claims `iss`, `aud`, `sub`, `tenant`, `role`, `iat`, `nbf`, and `exp`.
|
|
49
|
+
The `evaluate` role also needs signed `action="use"` and `vendor_id`. Maximum
|
|
50
|
+
lifetime is five minutes. The tenant is taken only from the verified assertion.
|
|
51
|
+
The issuer key and the process environment are operator trust boundaries, not
|
|
52
|
+
customer identity proof.
|
|
53
|
+
|
|
54
|
+
Run the app locally with:
|
|
55
|
+
|
|
56
|
+
```powershell
|
|
57
|
+
$env:PYTHONPATH = 'C:\Users\chaus\OneDrive\Documents\ChatGPT\buyer-side governance Kinetic Gain\policy-as-code-engine\src'
|
|
58
|
+
python -m policy_as_code_engine.pilot_app
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The command requires the four mandatory variables above to use protected
|
|
62
|
+
routes. `GET /healthz` is liveness. `GET /readyz` checks the configured issuer,
|
|
63
|
+
database schema version, required tables and columns, and SQLite quick check;
|
|
64
|
+
it is **not** evidence that a hosted gateway, buyer
|
|
65
|
+
authority, egress policy, backup, or rollback exists.
|
|
66
|
+
|
|
67
|
+
## API surface
|
|
68
|
+
|
|
69
|
+
| Route | Role | Result |
|
|
70
|
+
| --- | --- | --- |
|
|
71
|
+
| `POST /v1/keys` | tenant admin | Enroll one synthetic buyer public key and receipt. |
|
|
72
|
+
| `POST /v1/keys/{key_id}/revoke` | tenant admin | Revoke key; next evaluation of its cards denies. |
|
|
73
|
+
| `POST /v1/cards` | tenant admin | Verify a signed positive v0.1 card, derive one scoped bundle, record hash and receipt. Duplicate tenant/bundle IDs are rejected. |
|
|
74
|
+
| `POST /v1/bundles/{bundle_id}/revoke` | tenant admin | Revoke the card bundle. |
|
|
75
|
+
| `PUT /v1/bundles/{bundle_id}/conditions/{condition_id}` | tenant admin | Record a condition assertion valid for at most seven days. |
|
|
76
|
+
| `POST /v1/bundles/{bundle_id}/evaluate` | tenant evaluator | Derive action and vendor from the signed assertion; return `allow` or `deny` plus a committed receipt ID. Request bodies are rejected. |
|
|
77
|
+
| `GET /v1/receipts` | tenant admin | Last 100 minimal receipts for that tenant only. |
|
|
78
|
+
|
|
79
|
+
The pilot has no generic `/bundles`, one-shot `/evaluate`, raw card read,
|
|
80
|
+
customer login, or remote fetch route. Its stored bundle descriptions can
|
|
81
|
+
contain the synthetic card's vendor text, so the database remains synthetic.
|
|
82
|
+
|
|
83
|
+
## Reproducible local drill
|
|
84
|
+
|
|
85
|
+
```powershell
|
|
86
|
+
Set-Location 'C:\Users\chaus\OneDrive\Documents\ChatGPT\buyer-side governance Kinetic Gain\policy-as-code-engine'
|
|
87
|
+
$env:PYTHONPATH = (Resolve-Path 'src').Path
|
|
88
|
+
python -m pytest -q tests/test_pilot_app.py
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The tests generate temporary issuer and buyer keys and a temporary SQLite
|
|
92
|
+
file. They exercise default deny, scoped allow, a deny-all rollback switch,
|
|
93
|
+
wrong issuer audience, expired or tampered assertions, cross-tenant lookup,
|
|
94
|
+
wrong vendor, role separation, signed-card tampering, condition state,
|
|
95
|
+
old/new key overlap, key/card revocation across a restart, strict body limits,
|
|
96
|
+
headerless evaluation bodies, missing schema tables, and a known-positive audit
|
|
97
|
+
write failure. A 503 on the injected receipt write
|
|
98
|
+
failure is the expected fail-closed outcome.
|
|
99
|
+
|
|
100
|
+
## What remains blocked
|
|
101
|
+
|
|
102
|
+
This SQLite file is not a shared multi-instance store; it has no backed-up
|
|
103
|
+
restore, retention scheduler, migration playbook, or transactional delivery
|
|
104
|
+
outbox to another audit sink. The pilot issuer is a configured synthetic key,
|
|
105
|
+
not enrolled customer identity; it has no issuer rotation or replay prevention
|
|
106
|
+
for write assertions. Key enrollment is an operator assertion and does not
|
|
107
|
+
verify a buyer's real-world authority. The local loopback guard is not a
|
|
108
|
+
hosted gateway, distributed rate limit, or network egress policy. No remote
|
|
109
|
+
vendor fetching or customer traffic should be enabled from this pilot.
|
|
110
|
+
|
|
111
|
+
For hosted use, see the separate workspace release plan and the live release
|
|
112
|
+
gates. A real rollback needs an immutable deployed prior revision, a traffic
|
|
113
|
+
switch, and a database backup or compatible forward repair demonstrated on
|
|
114
|
+
the named deployment target. Setting a local deny-all flag is only a local
|
|
115
|
+
control drill.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "policy-as-code-engine"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
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" }
|
|
@@ -70,7 +70,7 @@ Issues = "https://github.com/mizcausevic-dev/policy-as-code-engine/issues
|
|
|
70
70
|
packages = ["src/policy_as_code_engine"]
|
|
71
71
|
|
|
72
72
|
[tool.hatch.build.targets.sdist]
|
|
73
|
-
only-include = ["src", "tests", "examples", "LICENSE", "README.md"]
|
|
73
|
+
only-include = ["src", "tests", "examples", "docs", "LICENSE", "README.md"]
|
|
74
74
|
|
|
75
75
|
[tool.pytest.ini_options]
|
|
76
76
|
testpaths = ["tests"]
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Optional audit-stream-py integration.
|
|
3
3
|
|
|
4
|
-
When
|
|
5
|
-
|
|
4
|
+
When `AUDIT_STREAM_URL` is set to the sink base URL (or its `/events`
|
|
5
|
+
endpoint), this module fires governance events at the normalized endpoint.
|
|
6
|
+
`AUDIT_STREAM_TOKEN` supplies the sink's bearer credential.
|
|
6
7
|
Best-effort: a failed POST is logged, not raised — audit-stream outages
|
|
7
8
|
must never block policy registration or evaluation.
|
|
8
9
|
|
|
@@ -15,15 +16,17 @@ Event kinds this service emits:
|
|
|
15
16
|
is "deny"
|
|
16
17
|
policy_condition_asserted on a successful admin condition assertion
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
aeo-validator-service.audit_stream. Identical config envvars.
|
|
19
|
+
The sink contract is aligned with procurement-decision-api.audit_stream.
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
|
+
import ipaddress
|
|
24
25
|
import math
|
|
25
26
|
import os
|
|
27
|
+
import re
|
|
26
28
|
from typing import Any
|
|
29
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
27
30
|
|
|
28
31
|
import httpx
|
|
29
32
|
|
|
@@ -43,6 +46,43 @@ def base_url() -> str | None:
|
|
|
43
46
|
return raw.rstrip("/")
|
|
44
47
|
|
|
45
48
|
|
|
49
|
+
def events_url() -> str | None:
|
|
50
|
+
"""Normalize a sink base or exact `/events` URL without forwarding URL credentials."""
|
|
51
|
+
raw = base_url()
|
|
52
|
+
if raw is None:
|
|
53
|
+
return None
|
|
54
|
+
try:
|
|
55
|
+
parsed = urlsplit(raw)
|
|
56
|
+
hostname = parsed.hostname
|
|
57
|
+
except ValueError:
|
|
58
|
+
return None
|
|
59
|
+
if (
|
|
60
|
+
parsed.scheme not in {"http", "https"}
|
|
61
|
+
or not parsed.netloc
|
|
62
|
+
or parsed.username is not None
|
|
63
|
+
or parsed.password is not None
|
|
64
|
+
or parsed.query
|
|
65
|
+
or parsed.fragment
|
|
66
|
+
):
|
|
67
|
+
return None
|
|
68
|
+
if parsed.scheme == "http":
|
|
69
|
+
try:
|
|
70
|
+
if not ipaddress.ip_address(hostname or "").is_loopback:
|
|
71
|
+
return None
|
|
72
|
+
except ValueError:
|
|
73
|
+
return None
|
|
74
|
+
path = parsed.path.rstrip("/")
|
|
75
|
+
if not path.endswith("/events"):
|
|
76
|
+
path += "/events"
|
|
77
|
+
return urlunsplit((parsed.scheme, parsed.netloc, path, "", ""))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def audit_token() -> str | None:
|
|
81
|
+
"""Return only a token accepted by the sink's configured-token syntax."""
|
|
82
|
+
token = os.environ.get("AUDIT_STREAM_TOKEN", "")
|
|
83
|
+
return token if re.fullmatch(r"[!-~]{32,}", token) else None
|
|
84
|
+
|
|
85
|
+
|
|
46
86
|
def timeout_s() -> float:
|
|
47
87
|
"""Configured per-call timeout. Defaults to 2.5s."""
|
|
48
88
|
raw = os.environ.get("AUDIT_STREAM_TIMEOUT_S", "").strip()
|
|
@@ -62,8 +102,12 @@ async def emit(
|
|
|
62
102
|
payload: dict[str, Any],
|
|
63
103
|
) -> None:
|
|
64
104
|
"""Fire one event. Silent no-op when AUDIT_STREAM_URL is unset."""
|
|
65
|
-
|
|
66
|
-
|
|
105
|
+
if not is_enabled():
|
|
106
|
+
return
|
|
107
|
+
url = events_url()
|
|
108
|
+
token = audit_token()
|
|
109
|
+
if url is None or token is None:
|
|
110
|
+
print(f"audit-stream emit failed (kind={kind}): InvalidConfiguration", flush=True)
|
|
67
111
|
return
|
|
68
112
|
|
|
69
113
|
body = {
|
|
@@ -73,11 +117,18 @@ async def emit(
|
|
|
73
117
|
}
|
|
74
118
|
try:
|
|
75
119
|
response = await client.post(
|
|
76
|
-
|
|
120
|
+
url,
|
|
77
121
|
json=body,
|
|
122
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
123
|
+
follow_redirects=False,
|
|
78
124
|
timeout=timeout_s(),
|
|
79
125
|
)
|
|
80
126
|
response.raise_for_status()
|
|
127
|
+
except httpx.HTTPStatusError as err:
|
|
128
|
+
print(
|
|
129
|
+
f"audit-stream emit failed (kind={kind}): HTTPStatusError status={err.response.status_code}",
|
|
130
|
+
flush=True,
|
|
131
|
+
)
|
|
81
132
|
except (httpx.HTTPError, OSError) as err:
|
|
82
133
|
print(
|
|
83
134
|
f"audit-stream emit failed (kind={kind}): {type(err).__name__}",
|