|
| 1 | +# keon-sdk-python — Architecture Sentinels |
| 2 | + |
| 3 | +**Status:** SEALED |
| 4 | +**Scope:** Architecture + Contract Guardrails |
| 5 | +**Rule:** No merge to main with sentinel failures. |
| 6 | +**Layer:** Policy SDK — enforces that policy authority is sovereign and cannot be weakened. |
| 7 | + |
| 8 | +--- |
| 9 | + |
| 10 | +## What these sentinels protect |
| 11 | + |
| 12 | +Sentinels are non-negotiable guardrails that prevent architectural drift and authority weakening. |
| 13 | +They are designed to fail closed. |
| 14 | + |
| 15 | +## Required Suites |
| 16 | + |
| 17 | +- `pytest -q` must include the sentinel file. |
| 18 | +- CI must block merges if sentinel suites are skipped or fail. |
| 19 | +- The `sentinels` CI job gates `test`, which gates `lint`. |
| 20 | + |
| 21 | +## Failure Philosophy |
| 22 | + |
| 23 | +- **Fail-closed:** If authority cannot be verified, the system must refuse to proceed. |
| 24 | +- **No silent downgrade:** Contract drift must be explicit and reviewed. |
| 25 | +- **No bypass paths:** Forbidden shortcuts are treated as defects, not optimizations. |
| 26 | + |
| 27 | +## Sentinel Inventory |
| 28 | + |
| 29 | +> Update this list intentionally. Removing or weakening sentinels requires doctrine review. |
| 30 | +
|
| 31 | +**Guarantee:** DecideRequest + DecisionReceipt strict, timeout → `NetworkError`, policy authority cannot silently weaken. |
| 32 | + |
| 33 | +| File | ID | Invariant | |
| 34 | +|------|----|-----------| |
| 35 | +| `test_keon_sentinels.py` | KEON-SDK-1a | `DecideRequest` without `correlationId` fails at construction | |
| 36 | +| `test_keon_sentinels.py` | KEON-SDK-1a | Malformed `correlationId` (non-uuidv7) rejected at construction | |
| 37 | +| `test_keon_sentinels.py` | KEON-SDK-1a | `tenantId` and `actorId` both mandatory — absent cases fail | |
| 38 | +| `test_keon_sentinels.py` | KEON-SDK-1b | `DecisionReceipt` without `receiptId` fails at construction | |
| 39 | +| `test_keon_sentinels.py` | KEON-SDK-1b | `receiptId` not matching `^dr-<uuidv7>` rejected | |
| 40 | +| `test_keon_sentinels.py` | KEON-SDK-1b | `DecisionReceipt` without `decision` field fails | |
| 41 | +| `test_keon_sentinels.py` | KEON-SDK-1c | Gateway timeout → `NetworkError`; never silently allows on failure | |
| 42 | +| `test_keon_sentinels.py` | DRIFT-1 | `DecideRequest` required-field set matches approved hash (no silent additions/removals) | |
| 43 | +| `test_keon_sentinels.py` | DRIFT-2 | `DecisionReceipt` required-field set matches approved hash (no silent additions/removals) | |
| 44 | + |
| 45 | +--- |
| 46 | + |
| 47 | +## How to Run |
| 48 | + |
| 49 | +```bash |
| 50 | +cd keon-sdk-python |
| 51 | +pytest -q |
| 52 | +``` |
| 53 | + |
| 54 | +--- |
| 55 | + |
| 56 | +## Drift Tripwires |
| 57 | + |
| 58 | +`DRIFT-1` and `DRIFT-2` hash the set of required fields in `DecideRequest` and `DecisionReceipt` at test time using `model_fields`. If any required field is added or removed without updating the stored hash, CI fails. |
| 59 | + |
| 60 | +To update after an approved schema change: |
| 61 | +1. Make the intentional change to the Pydantic model. |
| 62 | +2. Compute the new hash: |
| 63 | + ```python |
| 64 | + python -c " |
| 65 | + import hashlib, json |
| 66 | + from keon_sdk.contracts import DecideRequest |
| 67 | + fields = sorted(n for n, i in DecideRequest.model_fields.items() if i.is_required()) |
| 68 | + print(hashlib.sha256(json.dumps(fields, sort_keys=True).encode()).hexdigest()[:16]) |
| 69 | + " |
| 70 | + ``` |
| 71 | +3. Update `_DECIDE_FIELDS_HASH` or `_RECEIPT_FIELDS_HASH` in `test_keon_sentinels.py`. |
| 72 | +4. Commit: `sentinel(drift): approve <what changed> — <reason>`. |
| 73 | + |
| 74 | +--- |
| 75 | + |
| 76 | +## Seal Record |
| 77 | + |
| 78 | +- **Tag:** `keon-sdk-sentinels-v1.0.0` |
| 79 | +- **Commit:** `6da04940a6e109db6c57fd12583d5ac9cc67ede2` |
| 80 | +- **Date:** 2026-02-22 |
| 81 | + |
0 commit comments