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:
tegwick 2026-08-29 12:52:49 +02:00
parent 624e43f554
commit 9c9528f5b2
29 changed files with 2121 additions and 26 deletions

View file

@ -0,0 +1,24 @@
"""approval-engine — PIP for the approval object."""
from .binding import binding_digest, canonical_binding
from .errors import (
ApprovalError,
Conflict,
DuplicateApprover,
NotFound,
StoreUnavailable,
Unprocessable,
)
from .store import Engine
__all__ = [
"Engine",
"ApprovalError",
"Conflict",
"DuplicateApprover",
"NotFound",
"StoreUnavailable",
"Unprocessable",
"binding_digest",
"canonical_binding",
]

144
approval_engine/api.py Normal file
View file

@ -0,0 +1,144 @@
"""HTTP surface. Introspection and mutation; never a decision; never consume."""
from __future__ import annotations
import json
from typing import Any, Callable
from .errors import ApprovalError
from .store import Engine
FORBIDDEN_DECISION_KEYS = frozenset({"effect", "decision", "allow", "deny"})
def _read_json(environ: dict[str, Any]) -> dict[str, Any]:
length = int(environ.get("CONTENT_LENGTH") or 0)
if length == 0:
return {}
raw = environ["wsgi.input"].read(length)
if not raw:
return {}
try:
data = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise ApprovalError("invalid json") from exc
if not isinstance(data, dict):
raise ApprovalError("json object required")
return data
def _assert_not_decision(payload: Any) -> None:
if isinstance(payload, dict):
bad = FORBIDDEN_DECISION_KEYS & set(payload)
if bad:
raise RuntimeError(f"decision-shaped keys leaked: {sorted(bad)}")
for value in payload.values():
_assert_not_decision(value)
elif isinstance(payload, list):
for item in payload:
_assert_not_decision(item)
class App:
def __init__(self, engine: Engine) -> None:
self.engine = engine
def __call__(self, environ: dict[str, Any], start_response: Callable) -> list[bytes]:
method = environ.get("REQUEST_METHOD", "GET").upper()
path = environ.get("PATH_INFO") or "/"
try:
status, body = self.dispatch(method, path, environ)
except ApprovalError as exc:
status, body = exc.http_status, {"error": exc.reason_code, "message": str(exc)}
except ValueError as exc:
status, body = 422, {"error": "unprocessable", "message": str(exc)}
_assert_not_decision(body)
payload = json.dumps(body, sort_keys=True).encode("utf-8")
start_response(
f"{status} {'OK' if status < 400 else 'ERROR'}",
[
("Content-Type", "application/json"),
("Content-Length", str(len(payload))),
],
)
return [payload]
def dispatch(self, method: str, path: str, environ: dict[str, Any]) -> tuple[int, dict[str, Any]]:
if path in ("/readyz", "/v1/readyz") and method == "GET":
self.engine.outbox_stats()
return 200, {"status": "ok", "store": "ok"}
if path == "/v1/cadence" and method == "GET":
return 200, self.engine.transition_counts() | {"form": "heartbeat-or-reconciliation"}
if path == "/v1/outbox/stats" and method == "GET":
return 200, self.engine.outbox_stats()
if path == "/v1/heartbeat" and method == "POST":
return 200, self.engine.emit_heartbeat()
if path == "/v1/approvals" and method == "POST":
data = _read_json(environ)
obj = self.engine.create(
data.get("binding") or {},
data.get("validity") or {},
int(data.get("required_count") or 1),
pdp_digest=data.get("pdp_digest"),
approval_id=data.get("id"),
)
return 201, obj.as_dict()
parts = path.strip("/").split("/")
if len(parts) >= 3 and parts[0] == "v1" and parts[1] == "approvals":
approval_id = parts[2]
rest = parts[3:]
if rest == ["consume"] or path.endswith("/consume"):
return 404, {"error": "not_found", "message": "consume is not implemented"}
if not rest and method == "GET":
return 200, self.engine.get(approval_id).as_dict()
if rest == ["claim"] and method == "GET":
return 200, self.engine.claim(approval_id)
if rest == ["entries"] and method == "POST":
data = _read_json(environ)
obj = self.engine.add_entry(
approval_id,
data.get("subject_id") or "",
assurance=data.get("assurance"),
evidence_ref=data.get("evidence_ref"),
)
return 200, obj.as_dict()
if rest == ["revoke"] and method == "POST":
return 200, self.engine.revoke(approval_id).as_dict()
if rest == ["supersede"] and method == "POST":
data = _read_json(environ)
return 200, self.engine.supersede(approval_id, data.get("successor_id"))
if "check" in path or path.endswith("/authorize") or path.endswith("/consume"):
return 404, {"error": "not_found", "message": "no such surface"}
return 404, {"error": "not_found", "message": path}
def call(app: App, method: str, path: str, body: dict[str, Any] | None = None) -> tuple[int, dict[str, Any]]:
"""In-process WSGI helper for tests."""
raw = json.dumps(body or {}).encode("utf-8") if body is not None else b""
environ = {
"REQUEST_METHOD": method,
"PATH_INFO": path,
"wsgi.input": _Bytes(raw),
"CONTENT_LENGTH": str(len(raw)) if body is not None else "0",
"QUERY_STRING": "",
}
status_headers: list[tuple[str, list]] = []
def start_response(status: str, headers: list[tuple[str, str]]) -> None:
status_headers.append((status, headers))
result = b"".join(app(environ, start_response))
code = int(status_headers[0][0].split()[0])
return code, json.loads(result.decode("utf-8"))
class _Bytes:
def __init__(self, data: bytes) -> None:
self._data = data
def read(self, n: int = -1) -> bytes:
if n < 0:
out, self._data = self._data, b""
return out
out, self._data = self._data[:n], self._data[n:]
return out

View 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

28
approval_engine/cli.py Normal file
View file

@ -0,0 +1,28 @@
from __future__ import annotations
import argparse
from wsgiref.simple_server import make_server
from .api import App
from .store import Engine
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="approval-engine")
sub = parser.add_subparsers(dest="cmd", required=True)
serve = sub.add_parser("serve", help="serve the introspection API")
serve.add_argument("--db", default="approvals.sqlite")
serve.add_argument("--host", default="127.0.0.1")
serve.add_argument("--port", type=int, default=8787)
args = parser.parse_args(argv)
if args.cmd == "serve":
engine = Engine(args.db)
httpd = make_server(args.host, args.port, App(engine))
print(f"approval-engine on http://{args.host}:{args.port} db={args.db}")
httpd.serve_forever()
return 0
return 2
if __name__ == "__main__":
raise SystemExit(main())

28
approval_engine/errors.py Normal file
View file

@ -0,0 +1,28 @@
class ApprovalError(Exception):
http_status = 400
reason_code = "error"
class NotFound(ApprovalError):
http_status = 404
reason_code = "not_found"
class Conflict(ApprovalError):
http_status = 409
reason_code = "conflict"
class DuplicateApprover(ApprovalError):
http_status = 409
reason_code = "duplicate_approver"
class Unprocessable(ApprovalError):
http_status = 422
reason_code = "unprocessable"
class StoreUnavailable(ApprovalError):
http_status = 503
reason_code = "store_unavailable"

715
approval_engine/store.py Normal file
View file

@ -0,0 +1,715 @@
"""SQLite-backed approval object, closed machine, local outbox.
All mutations run in BEGIN IMMEDIATE and insert the outbox row before COMMIT.
There is no public consume; _cas_consume exists only so use-class emission
can be tested without guessing GH-WP-0002-T06.
"""
from __future__ import annotations
import json
import sqlite3
import threading
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Callable
from uuid import uuid4
from .binding import binding_digest, canonical_binding, require_digest
from .errors import (
Conflict,
DuplicateApprover,
NotFound,
StoreUnavailable,
Unprocessable,
)
ISSUER = "approval-engine"
CLAIM_SCHEMA = "0.1"
YIELDS_TO = "net-kingdom taxonomy request-claim schema (statute §17; unassigned)"
DEFAULT_FRESHNESS_TTL = 30
AUDIT_SCHEMA = "audit-core.event.v1alpha1"
SOURCE = "approval-engine"
SCOPE = "netkingdom-approvals"
EVENT_CLASSES = ("issuance", "use", "supersession", "revocation", "heartbeat")
SCHEMA = """
CREATE TABLE IF NOT EXISTS approvals (
id TEXT PRIMARY KEY,
status TEXT NOT NULL,
binding_json TEXT NOT NULL,
binding_digest TEXT NOT NULL,
pdp_digest TEXT,
actor TEXT NOT NULL,
principal TEXT NOT NULL,
action TEXT NOT NULL,
purpose TEXT NOT NULL,
target_json TEXT NOT NULL,
not_before TEXT NOT NULL,
expires_at TEXT NOT NULL,
required_count INTEGER NOT NULL,
superseded_by TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS entries (
approval_id TEXT NOT NULL,
subject_id TEXT NOT NULL,
approved_at TEXT NOT NULL,
assurance TEXT,
evidence_ref TEXT,
PRIMARY KEY (approval_id, subject_id)
);
CREATE TABLE IF NOT EXISTS outbox (
event_id TEXT PRIMARY KEY,
class TEXT NOT NULL,
approval_id TEXT,
payload_json TEXT NOT NULL,
created_at TEXT NOT NULL,
drained_at TEXT
);
"""
ACTIVE = ("requested", "approved")
MUTABLE = ("requested", "approved")
def utc_now(clock: Callable[[], datetime] | None = None) -> datetime:
if clock is not None:
now = clock()
else:
now = datetime.now(timezone.utc)
if now.tzinfo is None:
now = now.replace(tzinfo=timezone.utc)
return now.astimezone(timezone.utc).replace(microsecond=0)
def iso(ts: datetime) -> str:
return ts.astimezone(timezone.utc).replace(microsecond=0).isoformat()
def parse_iso(value: str) -> datetime:
return datetime.fromisoformat(value.replace("Z", "+00:00"))
@dataclass
class Approval:
id: str
status: str
binding: dict[str, Any]
binding_digest: str
pdp_digest: str | None
actor: str
principal: str
action: str
purpose: str
target: dict[str, Any]
not_before: str
expires_at: str
required_count: int
superseded_by: str | None
created_at: str
updated_at: str
entries: list[dict[str, Any]]
def as_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"status": self.status,
"binding": {
**self.binding,
"digest": self.binding_digest,
**({"pdp_digest": self.pdp_digest} if self.pdp_digest else {}),
},
"validity": {"not_before": self.not_before, "expires_at": self.expires_at},
"required_count": self.required_count,
"entries": self.entries,
"superseded_by": self.superseded_by,
"created_at": self.created_at,
"updated_at": self.updated_at,
}
class Engine:
def __init__(
self,
path: str | Path = ":memory:",
*,
clock: Callable[[], datetime] | None = None,
freshness_ttl: int = DEFAULT_FRESHNESS_TTL,
fail_outbox: bool = False,
tenant: str = "platform",
) -> None:
self.path = str(path)
self.clock = clock
self.freshness_ttl = freshness_ttl
self.fail_outbox = fail_outbox
self.tenant = tenant
self._local = threading.local()
self._init_schema()
def _connect(self) -> sqlite3.Connection:
try:
conn = sqlite3.connect(self.path, timeout=5, check_same_thread=False)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys=ON")
conn.execute("PRAGMA busy_timeout=5000")
if self.path != ":memory:":
conn.execute("PRAGMA journal_mode=WAL")
return conn
except sqlite3.Error as exc:
raise StoreUnavailable(str(exc)) from exc
def _conn(self) -> sqlite3.Connection:
conn = getattr(self._local, "conn", None)
if conn is None:
conn = self._connect()
self._local.conn = conn
return conn
def _init_schema(self) -> None:
conn = self._conn()
try:
conn.executescript(SCHEMA)
conn.commit()
except sqlite3.Error as exc:
raise StoreUnavailable(str(exc)) from exc
def close(self) -> None:
conn = getattr(self._local, "conn", None)
if conn is not None:
conn.close()
self._local.conn = None
def now(self) -> datetime:
return utc_now(self.clock)
def create(
self,
binding: dict[str, Any],
validity: dict[str, str],
required_count: int = 1,
*,
pdp_digest: str | None = None,
approval_id: str | None = None,
) -> Approval:
if required_count < 1:
raise Unprocessable("required_count must be >= 1")
canon = canonical_binding(binding)
digest = binding_digest(canon)
pdp = require_digest(pdp_digest)
not_before = validity.get("not_before") or iso(self.now())
expires_at = validity.get("expires_at")
if not expires_at:
raise Unprocessable("validity.expires_at is required")
if parse_iso(expires_at) <= parse_iso(not_before):
raise Unprocessable("expires_at must be after not_before")
aid = approval_id or str(uuid4())
now = iso(self.now())
conn = self._conn()
try:
conn.execute("BEGIN IMMEDIATE")
conn.execute(
"""INSERT INTO approvals (
id, status, binding_json, binding_digest, pdp_digest,
actor, principal, action, purpose, target_json,
not_before, expires_at, required_count, superseded_by,
created_at, updated_at
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
aid,
"requested",
json.dumps(canon, sort_keys=True),
digest,
pdp,
canon["actor"],
canon["principal"],
canon["action"],
canon["purpose"],
json.dumps(canon["target"], sort_keys=True),
not_before,
expires_at,
required_count,
None,
now,
now,
),
)
conn.commit()
except sqlite3.IntegrityError as exc:
conn.rollback()
raise Conflict("approval id already exists") from exc
except sqlite3.Error as exc:
conn.rollback()
raise StoreUnavailable(str(exc)) from exc
except Exception:
conn.rollback()
raise
return self.get(aid)
def get(self, approval_id: str) -> Approval:
conn = self._conn()
try:
row = conn.execute("SELECT * FROM approvals WHERE id=?", (approval_id,)).fetchone()
except sqlite3.Error as exc:
raise StoreUnavailable(str(exc)) from exc
if row is None:
raise NotFound(approval_id)
return self._hydrate(row, persist_expiry=True)
def _hydrate(self, row: sqlite3.Row, persist_expiry: bool) -> Approval:
entries = [
{
"subject_id": e["subject_id"],
"approved_at": e["approved_at"],
"assurance": e["assurance"],
"evidence_ref": e["evidence_ref"],
}
for e in self._conn()
.execute(
"SELECT subject_id, approved_at, assurance, evidence_ref "
"FROM entries WHERE approval_id=? ORDER BY approved_at",
(row["id"],),
)
.fetchall()
]
status = row["status"]
if persist_expiry and status in ACTIVE:
now = self.now()
if now >= parse_iso(row["expires_at"]):
status = self._expire(row["id"], status)
binding = json.loads(row["binding_json"])
return Approval(
id=row["id"],
status=status,
binding=binding,
binding_digest=row["binding_digest"],
pdp_digest=row["pdp_digest"],
actor=row["actor"],
principal=row["principal"],
action=row["action"],
purpose=row["purpose"],
target=json.loads(row["target_json"]),
not_before=row["not_before"],
expires_at=row["expires_at"],
required_count=row["required_count"],
superseded_by=row["superseded_by"],
created_at=row["created_at"],
updated_at=row["updated_at"],
entries=entries,
)
def _expire(self, approval_id: str, from_status: str) -> str:
conn = self._conn()
now = iso(self.now())
try:
conn.execute("BEGIN IMMEDIATE")
cur = conn.execute(
"UPDATE approvals SET status='expired', updated_at=? "
"WHERE id=? AND status=?",
(now, approval_id, from_status),
)
conn.commit()
return "expired" if cur.rowcount == 1 else from_status
except sqlite3.Error:
conn.rollback()
return from_status
def add_entry(
self,
approval_id: str,
subject_id: str,
*,
assurance: str | None = None,
evidence_ref: str | None = None,
) -> Approval:
if not subject_id:
raise Unprocessable("subject_id is required")
conn = self._conn()
now = iso(self.now())
try:
conn.execute("BEGIN IMMEDIATE")
row = conn.execute("SELECT * FROM approvals WHERE id=?", (approval_id,)).fetchone()
if row is None:
conn.rollback()
raise NotFound(approval_id)
if row["status"] not in MUTABLE:
conn.rollback()
raise Conflict(f"cannot add entries in status {row['status']}")
try:
conn.execute(
"INSERT INTO entries (approval_id, subject_id, approved_at, assurance, evidence_ref) "
"VALUES (?,?,?,?,?)",
(approval_id, subject_id, now, assurance, evidence_ref),
)
except sqlite3.IntegrityError as exc:
conn.rollback()
raise DuplicateApprover(subject_id) from exc
count = conn.execute(
"SELECT COUNT(*) FROM entries WHERE approval_id=?", (approval_id,)
).fetchone()[0]
became_approved = False
if row["status"] == "requested" and count >= row["required_count"]:
conn.execute(
"UPDATE approvals SET status='approved', updated_at=? WHERE id=? AND status='requested'",
(now, approval_id),
)
became_approved = True
else:
conn.execute(
"UPDATE approvals SET updated_at=? WHERE id=?", (now, approval_id)
)
if became_approved:
self._outbox_insert(
conn,
"issuance",
approval_id,
actor=row["actor"],
extra={"binding_digest": row["binding_digest"], "required_count": row["required_count"]},
)
conn.commit()
except sqlite3.Error as exc:
conn.rollback()
raise StoreUnavailable(str(exc)) from exc
except Exception:
conn.rollback()
raise
return self.get(approval_id)
def revoke(self, approval_id: str) -> Approval:
return self._terminal(approval_id, "revoked", "revocation")
def supersede(self, approval_id: str, successor_id: str | None = None) -> dict[str, Any]:
successor_id = successor_id or str(uuid4())
conn = self._conn()
now = iso(self.now())
try:
conn.execute("BEGIN IMMEDIATE")
row = conn.execute("SELECT * FROM approvals WHERE id=?", (approval_id,)).fetchone()
if row is None:
conn.rollback()
raise NotFound(approval_id)
if row["status"] not in MUTABLE:
conn.rollback()
raise Conflict(f"cannot supersede in status {row['status']}")
cur = conn.execute(
"UPDATE approvals SET status='superseded', superseded_by=?, updated_at=? "
"WHERE id=? AND status IN ('requested','approved')",
(successor_id, now, approval_id),
)
if cur.rowcount != 1:
conn.rollback()
raise Conflict("supersession lost the compare-and-swap")
existing = conn.execute("SELECT id FROM approvals WHERE id=?", (successor_id,)).fetchone()
created_successor = False
if existing is None:
conn.execute(
"""INSERT INTO approvals (
id, status, binding_json, binding_digest, pdp_digest,
actor, principal, action, purpose, target_json,
not_before, expires_at, required_count, superseded_by,
created_at, updated_at
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
successor_id,
"requested",
row["binding_json"],
row["binding_digest"],
row["pdp_digest"],
row["actor"],
row["principal"],
row["action"],
row["purpose"],
row["target_json"],
row["not_before"],
row["expires_at"],
row["required_count"],
None,
now,
now,
),
)
created_successor = True
self._outbox_insert(
conn,
"supersession",
approval_id,
actor=row["actor"],
extra={
"binding_digest": row["binding_digest"],
"superseded_by": successor_id,
},
)
conn.commit()
except sqlite3.Error as exc:
conn.rollback()
raise StoreUnavailable(str(exc)) from exc
except Exception:
conn.rollback()
raise
return {
"superseded": self.get(approval_id).as_dict(),
"successor_id": successor_id,
"successor_created": created_successor,
}
def _terminal(self, approval_id: str, status: str, event_class: str) -> Approval:
conn = self._conn()
now = iso(self.now())
try:
conn.execute("BEGIN IMMEDIATE")
row = conn.execute("SELECT * FROM approvals WHERE id=?", (approval_id,)).fetchone()
if row is None:
conn.rollback()
raise NotFound(approval_id)
cur = conn.execute(
"UPDATE approvals SET status=?, updated_at=? "
"WHERE id=? AND status IN ('requested','approved')",
(status, now, approval_id),
)
if cur.rowcount != 1:
conn.rollback()
raise Conflict(f"cannot {status} from status {row['status']}")
self._outbox_insert(
conn,
event_class,
approval_id,
actor=row["actor"],
extra={"binding_digest": row["binding_digest"]},
)
conn.commit()
except sqlite3.Error as exc:
conn.rollback()
raise StoreUnavailable(str(exc)) from exc
except Exception:
conn.rollback()
raise
return self.get(approval_id)
def _cas_consume(self, approval_id: str) -> Approval:
"""Unexported seam. Do not wire to HTTP. Blocked on GH-WP-0002-T06."""
conn = self._conn()
now = iso(self.now())
try:
conn.execute("BEGIN IMMEDIATE")
row = conn.execute("SELECT * FROM approvals WHERE id=?", (approval_id,)).fetchone()
if row is None:
conn.rollback()
raise NotFound(approval_id)
if row["status"] != "approved":
conn.rollback()
raise Conflict(f"cannot consume from status {row['status']}")
if self.now() < parse_iso(row["not_before"]) or self.now() >= parse_iso(row["expires_at"]):
conn.rollback()
raise Conflict("cannot consume outside validity window")
cur = conn.execute(
"UPDATE approvals SET status='consumed', updated_at=? "
"WHERE id=? AND status='approved'",
(now, approval_id),
)
if cur.rowcount != 1:
conn.rollback()
raise Conflict("consumption lost the compare-and-swap")
self._outbox_insert(
conn,
"use",
approval_id,
actor=row["actor"],
extra={"binding_digest": row["binding_digest"]},
)
conn.commit()
except sqlite3.Error as exc:
conn.rollback()
raise StoreUnavailable(str(exc)) from exc
except Exception:
conn.rollback()
raise
return self.get(approval_id)
def _outbox_insert(
self,
conn: sqlite3.Connection,
event_class: str,
approval_id: str | None,
*,
actor: str | None,
extra: dict[str, Any] | None = None,
) -> str:
if self.fail_outbox:
raise StoreUnavailable("outbox insert failed")
if event_class not in EVENT_CLASSES:
raise Unprocessable(f"unknown event class {event_class}")
event_id = str(uuid4())
created = iso(self.now())
resource = f"approval:{approval_id}" if approval_id else "approval-engine:heartbeat"
details = {"class": event_class, **(extra or {})}
if approval_id:
details["approval_id"] = approval_id
payload = {
"schema_version": AUDIT_SCHEMA,
"event_id": event_id,
"observed_at": created,
"tenant": self.tenant,
"scope": SCOPE,
"source": SOURCE,
"actor": actor,
"action": f"approval.{event_class}",
"resource": resource,
"outcome": "success",
"reason": None,
"details": details,
}
conn.execute(
"INSERT INTO outbox (event_id, class, approval_id, payload_json, created_at, drained_at) "
"VALUES (?,?,?,?,?,NULL)",
(event_id, event_class, approval_id, json.dumps(payload, sort_keys=True), created),
)
return event_id
def claim(self, approval_id: str) -> dict[str, Any]:
obj = self.get(approval_id)
observed = self.now()
not_after = observed + timedelta(seconds=self.freshness_ttl)
state, valid_now, consumed, reason = self._evaluate(obj, observed)
binding = {
**canonical_binding(obj.binding),
"digest": obj.binding_digest,
}
if obj.pdp_digest:
binding["pdp_digest"] = obj.pdp_digest
return {
"schema_version": CLAIM_SCHEMA,
"kind": "approval-claim",
"yields_to": YIELDS_TO,
"issuer": ISSUER,
"approval_id": obj.id,
"state": state,
"valid_now": valid_now,
"consumed": consumed,
"binding": binding,
"freshness": {
"observed_at": iso(observed),
"ttl_seconds": self.freshness_ttl,
"not_after": iso(not_after),
},
"validity": {"not_before": obj.not_before, "expires_at": obj.expires_at},
"reason_code": reason,
}
def _evaluate(self, obj: Approval, now: datetime) -> tuple[str, bool, bool, str]:
consumed = obj.status == "consumed"
if obj.status == "revoked":
return "revoked", False, consumed, "revoked"
if obj.status == "superseded":
return "superseded", False, consumed, "superseded"
if obj.status == "expired" or now >= parse_iso(obj.expires_at):
return "expired", False, consumed, "expired"
if obj.status == "consumed":
return "consumed", False, True, "consumed"
if obj.status == "requested":
return "requested", False, False, "requested"
if obj.status == "approved":
if now < parse_iso(obj.not_before):
return "approved", False, False, "not_yet_valid"
if len({e["subject_id"] for e in obj.entries}) < obj.required_count:
return "approved", False, False, "insufficient_approvers"
return "valid", True, False, "ok"
return obj.status, False, consumed, obj.status
def emit_heartbeat(self) -> dict[str, Any]:
conn = self._conn()
counts = self.transition_counts()
counts["heartbeat"] += 1
try:
conn.execute("BEGIN IMMEDIATE")
event_id = self._outbox_insert(
conn,
"heartbeat",
None,
actor=None,
extra={"assertion": "nothing-to-report", "counts": counts},
)
conn.commit()
except sqlite3.Error as exc:
conn.rollback()
raise StoreUnavailable(str(exc)) from exc
except Exception:
conn.rollback()
raise
return {"event_id": event_id, "assertion": "nothing-to-report", "counts": counts}
def transition_counts(self) -> dict[str, int]:
conn = self._conn()
try:
rows = conn.execute("SELECT class, COUNT(*) AS n FROM outbox GROUP BY class").fetchall()
except sqlite3.Error as exc:
raise StoreUnavailable(str(exc)) from exc
counts = {c: 0 for c in EVENT_CLASSES}
for row in rows:
counts[row["class"]] = row["n"]
return counts
def outbox_stats(self) -> dict[str, Any]:
conn = self._conn()
now = self.now()
try:
rows = conn.execute(
"SELECT event_id, class, created_at, drained_at FROM outbox"
).fetchall()
except sqlite3.Error as exc:
raise StoreUnavailable(str(exc)) from exc
pending = [r for r in rows if r["drained_at"] is None]
ages = [(now - parse_iso(r["created_at"])).total_seconds() for r in pending]
return {
"total": len(rows),
"pending": len(pending),
"max_age_seconds": max(ages) if ages else 0,
"counts": self.transition_counts(),
}
def undrained(self) -> list[dict[str, Any]]:
conn = self._conn()
try:
rows = conn.execute(
"SELECT event_id, class, approval_id, payload_json, created_at "
"FROM outbox WHERE drained_at IS NULL ORDER BY created_at"
).fetchall()
except sqlite3.Error as exc:
raise StoreUnavailable(str(exc)) from exc
return [
{
"event_id": r["event_id"],
"class": r["class"],
"approval_id": r["approval_id"],
"payload": json.loads(r["payload_json"]),
"created_at": r["created_at"],
}
for r in rows
]
def drain(self, sink: Callable[[dict[str, Any]], None]) -> dict[str, int]:
"""Deliver undrained payloads. Sink failure leaves the row pending.
Object mutations are already committed; this must not roll them back.
"""
delivered = 0
failed = 0
conn = self._conn()
for item in self.undrained():
try:
sink(item["payload"])
except Exception:
failed += 1
continue
try:
conn.execute(
"UPDATE outbox SET drained_at=? WHERE event_id=? AND drained_at IS NULL",
(iso(self.now()), item["event_id"]),
)
conn.commit()
delivered += 1
except sqlite3.Error as exc:
conn.rollback()
raise StoreUnavailable(str(exc)) from exc
return {"delivered": delivered, "failed": failed}