Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
82 lines
4.2 KiB
Python
82 lines
4.2 KiB
Python
"""Authenticated get-by-id and human entry transport; no policy or consume API.
|
|
|
|
The caller owes entitlement, durable presentation/disposition and audit custody
|
|
before invoking add_entry. This adapter is deliberately not a browser route.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
import re
|
|
import time
|
|
|
|
from .approval_client import ApprovalEngineError, EntryResult, is_success
|
|
from .http_transport import JSONTransport, TransportError, fixed_origin
|
|
from .oidc import HumanSession
|
|
from .provenance import assert_human_control_dischargeable
|
|
|
|
|
|
class ApprovalHTTPClient:
|
|
def __init__(self, origin: str, session: HumanSession, *, transport=None, clock=time.time,
|
|
allow_internal_http=False):
|
|
self.origin = fixed_origin(origin, allow_internal_http=allow_internal_http)
|
|
assert_human_control_dischargeable(session.principal_type)
|
|
if session.tenant.value != "tenant:platform":
|
|
raise ValueError("platform approver required")
|
|
self.session = session
|
|
self.transport = transport or JSONTransport(allow_internal_http=allow_internal_http)
|
|
self.clock = clock
|
|
|
|
def _call(self, method: str, approval_id: str, suffix="") -> dict:
|
|
if not isinstance(approval_id, str) or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]{0,127}", approval_id):
|
|
raise ValueError("invalid approval id")
|
|
if self.session.expires_at <= self.clock():
|
|
raise ApprovalEngineError(401, "session_expired")
|
|
try:
|
|
status, data = self.transport.request(method, self.origin + "/v1/approvals/" + approval_id + suffix,
|
|
headers={"Authorization": "Bearer " + self.session.access_token,
|
|
"Content-Type": "application/json"}, body=b"{}" if method == "POST" else None)
|
|
except TransportError:
|
|
# A failed POST transport may already have committed. Never retry it
|
|
# automatically or manufacture a disposition from this uncertainty.
|
|
raise ApprovalEngineError(502, "upstream_unavailable") from None
|
|
if status != 200:
|
|
reason = data.get("error")
|
|
known = {"unauthenticated", "forbidden", "not_found", "conflict",
|
|
"duplicate_approver", "unprocessable", "store_unavailable"}
|
|
raise ApprovalEngineError(status, reason if isinstance(reason, str) and reason in known else "upstream_refused")
|
|
binding = data.get("binding")
|
|
if (data.get("id") != approval_id or not isinstance(binding, dict)
|
|
or binding.get("human_control") is not True
|
|
or not isinstance(binding.get("digest"), str)
|
|
or not re.fullmatch(r"sha256:[0-9a-f]{64}", binding["digest"])
|
|
or data.get("status") not in {"requested", "approved", "consumed", "expired", "revoked", "superseded"}):
|
|
raise ApprovalEngineError(502, "invalid_approval_response")
|
|
return data
|
|
|
|
def get_approval(self, approval_id: str) -> dict:
|
|
return self._call("GET", approval_id)
|
|
|
|
def add_entry(self, approval_id: str) -> EntryResult:
|
|
self.get_approval(approval_id) # Require the declared human-control object before mutation.
|
|
duplicate = False
|
|
try:
|
|
data = self._call("POST", approval_id, "/entries")
|
|
except ApprovalEngineError as exc:
|
|
if not is_success(exc):
|
|
raise
|
|
duplicate = True
|
|
data = self.get_approval(approval_id)
|
|
entries = data.get("entries")
|
|
if not isinstance(entries, list) or any(not isinstance(e, dict) for e in entries):
|
|
raise ApprovalEngineError(502, "entry_correlation_missing")
|
|
matches = [e for e in entries if e.get("subject_id") == self.session.subject]
|
|
try:
|
|
if len(matches) != 1 or matches[0].get("principal_type") != "human":
|
|
raise ValueError()
|
|
at = matches[0]["approved_at"]
|
|
if not isinstance(at, str) or datetime.fromisoformat(at.replace("Z", "+00:00")).tzinfo is None:
|
|
raise ValueError()
|
|
except (KeyError, TypeError, ValueError):
|
|
raise ApprovalEngineError(502, "entry_correlation_missing") from None
|
|
return EntryResult(approval_id, self.session.subject, at, data["status"], duplicate)
|