Add verified browser login and human approval HTTP client
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
4e103f62a0
commit
0e48355b9f
16 changed files with 1365 additions and 39 deletions
82
informed_decision/approval_http.py
Normal file
82
informed_decision/approval_http.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
"""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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue