Implement approval engine production readiness
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a05e2e-805b-7042-a750-71f473bceea2
This commit is contained in:
parent
ebce5abb27
commit
2bd2d19a98
30 changed files with 1679 additions and 53 deletions
|
|
@ -5,17 +5,25 @@ from __future__ import annotations
|
|||
import json
|
||||
from typing import Any, Callable
|
||||
|
||||
from .errors import ApprovalError
|
||||
from .auth import Authenticator, DenyAllAuthenticator, Identity
|
||||
from .errors import ApprovalError, Forbidden
|
||||
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)
|
||||
try:
|
||||
length = int(environ.get("CONTENT_LENGTH") or 0)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ApprovalError("invalid content length") from exc
|
||||
if length < 0 or length > 256 * 1024:
|
||||
raise ApprovalError("request body is too large")
|
||||
if length == 0:
|
||||
return {}
|
||||
raw = environ["wsgi.input"].read(length)
|
||||
if len(raw) != length:
|
||||
raise ApprovalError("truncated request body")
|
||||
if not raw:
|
||||
return {}
|
||||
try:
|
||||
|
|
@ -40,8 +48,24 @@ def _assert_not_decision(payload: Any) -> None:
|
|||
|
||||
|
||||
class App:
|
||||
def __init__(self, engine: Engine) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
engine: Engine,
|
||||
authenticator: Authenticator | None = None,
|
||||
*,
|
||||
require_persistent: bool = False,
|
||||
) -> None:
|
||||
self.engine = engine
|
||||
self.authenticator = authenticator or DenyAllAuthenticator()
|
||||
self.require_persistent = require_persistent
|
||||
|
||||
def identity(self, environ: dict[str, Any], scope: str) -> Identity:
|
||||
identity = self.authenticator.authenticate(
|
||||
environ.get("HTTP_AUTHORIZATION")
|
||||
).require(scope)
|
||||
if identity.tenant != self.engine.tenant:
|
||||
raise Forbidden("caller tenant does not match this approval store")
|
||||
return identity
|
||||
|
||||
def __call__(self, environ: dict[str, Any], start_response: Callable) -> list[bytes]:
|
||||
method = environ.get("REQUEST_METHOD", "GET").upper()
|
||||
|
|
@ -64,19 +88,38 @@ class App:
|
|||
return [payload]
|
||||
|
||||
def dispatch(self, method: str, path: str, environ: dict[str, Any]) -> tuple[int, dict[str, Any]]:
|
||||
if path in ("/healthz", "/v1/healthz") and method == "GET":
|
||||
return 200, {"status": "ok"}
|
||||
if path in ("/readyz", "/v1/readyz") and method == "GET":
|
||||
storage = self.engine.storage_status()
|
||||
ready = storage["schema_current"] and (
|
||||
storage["persistent"] or not self.require_persistent
|
||||
)
|
||||
self.engine.outbox_stats()
|
||||
return 200, {"status": "ok", "store": "ok"}
|
||||
return (200 if ready else 503), {
|
||||
"status": "ok" if ready else "unavailable",
|
||||
"store": "ok" if ready else "not-production-ready",
|
||||
}
|
||||
if path == "/v1/storage/status" and method == "GET":
|
||||
self.identity(environ, "approval:observe")
|
||||
return 200, self.engine.storage_status(integrity=True)
|
||||
if path == "/v1/cadence" and method == "GET":
|
||||
self.identity(environ, "approval:observe")
|
||||
return 200, self.engine.transition_counts() | {"form": "heartbeat-or-reconciliation"}
|
||||
if path == "/v1/outbox/stats" and method == "GET":
|
||||
self.identity(environ, "approval:observe")
|
||||
return 200, self.engine.outbox_stats()
|
||||
if path == "/v1/heartbeat" and method == "POST":
|
||||
self.identity(environ, "approval:emit")
|
||||
return 200, self.engine.emit_heartbeat()
|
||||
if path == "/v1/approvals" and method == "POST":
|
||||
identity = self.identity(environ, "approval:create")
|
||||
data = _read_json(environ)
|
||||
binding = data.get("binding") or {}
|
||||
if binding.get("actor") != identity.subject:
|
||||
raise Forbidden("binding.actor must match the authenticated subject")
|
||||
obj = self.engine.create(
|
||||
data.get("binding") or {},
|
||||
binding,
|
||||
data.get("validity") or {},
|
||||
int(data.get("required_count") or 1),
|
||||
pdp_digest=data.get("pdp_digest"),
|
||||
|
|
@ -88,24 +131,32 @@ class App:
|
|||
approval_id = parts[2]
|
||||
rest = parts[3:]
|
||||
if not rest and method == "GET":
|
||||
self.identity(environ, "approval:read")
|
||||
return 200, self.engine.get(approval_id).as_dict()
|
||||
if rest == ["claim"] and method == "GET":
|
||||
self.identity(environ, "approval:read")
|
||||
return 200, self.engine.claim(approval_id)
|
||||
if rest == ["entries"] and method == "POST":
|
||||
data = _read_json(environ)
|
||||
identity = self.identity(environ, "approval:approve")
|
||||
_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"),
|
||||
identity.subject,
|
||||
assurance=json.dumps(identity.assurance, sort_keys=True),
|
||||
evidence_ref=identity.evidence_ref,
|
||||
)
|
||||
return 200, obj.as_dict()
|
||||
if rest == ["revoke"] and method == "POST":
|
||||
self.identity(environ, "approval:revoke")
|
||||
return 200, self.engine.revoke(approval_id).as_dict()
|
||||
if rest == ["supersede"] and method == "POST":
|
||||
self.identity(environ, "approval:supersede")
|
||||
data = _read_json(environ)
|
||||
return 200, self.engine.supersede(approval_id, data.get("successor_id"))
|
||||
if rest == ["consume"] and method == "POST":
|
||||
identity = self.identity(environ, "approval:consume")
|
||||
if identity.principal_type not in {"service", "agent"}:
|
||||
raise Forbidden("consume requires a service or agent principal")
|
||||
data = _read_json(environ)
|
||||
return 200, self.engine.consume(
|
||||
approval_id,
|
||||
|
|
@ -117,7 +168,14 @@ class App:
|
|||
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]]:
|
||||
def call(
|
||||
app: App,
|
||||
method: str,
|
||||
path: str,
|
||||
body: dict[str, Any] | None = None,
|
||||
*,
|
||||
authorization: str | None = "Bearer test-token",
|
||||
) -> 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 = {
|
||||
|
|
@ -127,6 +185,8 @@ def call(app: App, method: str, path: str, body: dict[str, Any] | None = None) -
|
|||
"CONTENT_LENGTH": str(len(raw)) if body is not None else "0",
|
||||
"QUERY_STRING": "",
|
||||
}
|
||||
if authorization is not None:
|
||||
environ["HTTP_AUTHORIZATION"] = authorization
|
||||
status_headers: list[tuple[str, list]] = []
|
||||
|
||||
def start_response(status: str, headers: list[tuple[str, str]]) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue