Implement the engine spine: claim, outbox, machine, API
Contracts first (T02–T04): approval claim schema with issuer, freshness,
and binding digest; local transactional outbox wire; load-bearing cadence
as heartbeat or reconciliation (layer.yaml declared).
Then the object (T06–T08): SQLite closed state machine, CAS supersession,
distinct-approver fail-closed, revocation without holder cooperation,
outbox insert in the same transaction. Tests fail the mutation when
emission fails, and revoke while the drain sink is down.
Introspection GET /v1/approvals/{id}/claim is a PIP fact, not a decision.
No public consume (T05 waits on GH-WP-0002-T06). Canon T-06 coverage for
wrong binding, expiry, revoke, and supersede.
FLEX-WP-0017 T03 is unblocked on this object; T05 remains blocked only on
consumption ordering.
Assistant: grok
Assistant-Session: 01a04ceb-2057-7e20-b0f9-c282964d5dd9
This commit is contained in:
parent
624e43f554
commit
9c9528f5b2
29 changed files with 2121 additions and 26 deletions
45
approval_engine/binding.py
Normal file
45
approval_engine/binding.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
"""Canonical binding and native digest.
|
||||
|
||||
The native digest is SHA-256 over sorted-key JSON of five fields. It is not
|
||||
Go json.Marshal of a flex-auth CheckRequest — that value, when known at
|
||||
issue, is stored separately as pdp_digest.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
||||
|
||||
BINDING_FIELDS = ("action", "actor", "principal", "purpose", "target")
|
||||
|
||||
|
||||
def canonical_binding(binding: dict[str, Any]) -> dict[str, Any]:
|
||||
missing = [k for k in BINDING_FIELDS if k not in binding or binding[k] in (None, "")]
|
||||
if missing:
|
||||
raise ValueError(f"binding missing {missing}")
|
||||
if not isinstance(binding["target"], dict):
|
||||
raise ValueError("binding.target must be an object")
|
||||
return {k: binding[k] for k in BINDING_FIELDS}
|
||||
|
||||
|
||||
def canonical_json(value: Any) -> bytes:
|
||||
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode(
|
||||
"utf-8"
|
||||
)
|
||||
|
||||
|
||||
def binding_digest(binding: dict[str, Any]) -> str:
|
||||
payload = canonical_json(canonical_binding(binding))
|
||||
return "sha256:" + hashlib.sha256(payload).hexdigest()
|
||||
|
||||
|
||||
def require_digest(value: str | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
if not DIGEST_RE.match(value):
|
||||
raise ValueError("digest must match sha256:<64 lowercase hex>")
|
||||
return value
|
||||
Loading…
Add table
Add a link
Reference in a new issue