state-hub/api/services/informed_decision_contract.py
tegwick 49eeddbfed
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 27s
Add a dev memo-forwarding fixture and deduplicated intake notifications.
Pose, replay, and conflict stay on an injected transport with local retry
evidence. Notification receipts keep only memo id, question, class, and
submitter, and a resolve conflict stays readable.

Assistant: grok
Assistant-Session: 01a10329-31cb-78d3-b7a3-789e1fb83139
2026-10-03 21:25:46 +02:00

382 lines
13 KiB
Python

"""Pure contract for informed-decision forwarding and notifications.
Dev and request validation only. This module does not read a token, open a
socket, register a submitter, or decide a hub decision. ``drafted_by`` is
asserted provenance; it is never an authenticated identity.
"""
from __future__ import annotations
import hashlib
import json
import re
from dataclasses import dataclass
from typing import Any, Mapping
KIND = "informed-decision.memo-intake"
VERSION = 1
OVERVIEW_LINE = "The signed-in overview is the authoritative review entry point."
PROVENANCE_NOTE = (
"drafted_by is asserted provenance, not authenticated identity. "
"A forwarded token proves only the hub submitter."
)
RESOLVING_VERBS = frozenset({"accept", "decline"})
NOTIFICATION_FIELDS = ("memo_id", "question", "memo_class", "submitter")
MAX_QUESTION = 400
MAX_DRAFTED_BY = 200
MAX_PACKET_BYTES = 256 * 1024
MAX_ENVELOPE_BYTES = 1024 * 1024
MAX_PACKET_ITEMS = 32
MAX_NOTIFICATION_BYTES = 8192
_KEY_RE = re.compile(r"^[A-Za-z0-9._:-]{8,128}$")
_MEMO_ID_RE = re.compile(r"^[a-z0-9][a-z0-9-]{2,62}$")
_SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9-]{1,62}$")
_UUID_RE = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
_SHA_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
_ERROR_RE = re.compile(r"^[a-z0-9_]{1,64}$")
ENVELOPE_KEYS = {
"kind", "version", "idempotency_key", "memo_class", "review_group",
"drafted_by", "serves", "approval_intent", "memo", "packet",
}
ENVELOPE_REQUIRED = ENVELOPE_KEYS - {"drafted_by"}
INTENT_KEYS = {"action", "principal", "purpose", "target"}
TARGET_KEYS = {"id", "type", "system"}
PACKET_KEYS = {"item_id", "media_type", "content"}
MEDIA_TYPES = {"text/markdown", "text/plain", "application/json"}
# Shapes only. A hit is refused by code; the matched text is never echoed.
_SECRET_MARKERS = (
re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
re.compile(r"\bBearer\s+[A-Za-z0-9._~+/=-]{16,}"),
re.compile(r"\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}"),
re.compile(r"\b(hvs|hvb|hvr)\.[A-Za-z0-9_-]{20,}"),
re.compile(
r"\b(sk-(or-)?[A-Za-z0-9_-]{20,}|ghp_[A-Za-z0-9]{30,}|"
r"github_pat_[A-Za-z0-9_]{30,}|glpat-[A-Za-z0-9_-]{20,}|"
r"AKIA[0-9A-Z]{16}|xox[abpr]-[A-Za-z0-9-]{10,})"
),
re.compile(
r"(?i)\b(password|passwd|client_secret|secret|api[_-]?key|access[_-]?token|token)"
r"\s*[:=]\s*['\"]?(?=[A-Za-z0-9_+.=-]*\d)[A-Za-z0-9_+.=-]{12,}(?=['\"\s,;)]|$)"
),
re.compile(r"://[^/@\s:]+:[^/@\s]+@"),
)
class ContractError(ValueError):
def __init__(self, code: str, status: int = 400) -> None:
super().__init__(code)
self.code = code
self.status = status
def canonical(value: Any) -> str:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
def digest(value: Any) -> str:
return hashlib.sha256(canonical(value).encode("utf-8")).hexdigest()
def scan_secrets(value: Any) -> None:
for text in _strings(value):
for pattern in _SECRET_MARKERS:
if pattern.search(text):
raise ContractError("secret_marker")
def stable_idempotency_key(decision_id: str, memo_id: str) -> str:
_uuid(decision_id, "hub_decision_id")
_memo_id(memo_id)
return f"state-hub.{decision_id}.{memo_id}"
def resolution_applies(verb: str, serves: Mapping | None) -> bool:
"""Terminal human accept/decline with resolves=on-answer only."""
if verb not in RESOLVING_VERBS:
return False
if not isinstance(serves, Mapping):
return False
return serves.get("resolves") == "on-answer"
def correlate_resolution(
memo_id: str, *, post_status: int | None, existing: Mapping | None
) -> str:
"""Classify a resolve attempt. A 409 is a conflict only after the existing
resolution was read; an unreadable 409 stays retryable."""
_memo_id(memo_id)
if post_status is None:
return "failed"
if 200 <= post_status < 300:
return "resolved"
if post_status != 409:
return "failed"
if not isinstance(existing, Mapping):
return "failed"
rationale = existing.get("rationale")
if not isinstance(rationale, str):
return "failed"
if f"memo {memo_id} " in rationale:
return "already_resolved"
return "conflict"
def safe_error_code(value: Any, fallback: str) -> str:
if isinstance(value, str) and _ERROR_RE.match(value):
return value
return fallback
@dataclass(frozen=True)
class Notification:
memo_id: str
question: str
memo_class: str
submitter: str
def as_body(self) -> dict[str, str]:
return {
"memo_id": self.memo_id,
"question": self.question,
"memo_class": self.memo_class,
"submitter": self.submitter,
}
@property
def body_digest(self) -> str:
return digest(self.as_body())
def message_subject(self) -> str:
return f"Decision memo {self.memo_id}"
def message_text(self) -> str:
return OVERVIEW_LINE + "\n" + canonical(self.as_body())
def parse_delivery_id(value: str | None) -> str:
if value is None or value == "":
raise ContractError("delivery_id_required")
if not isinstance(value, str) or not _KEY_RE.match(value):
raise ContractError("delivery_id_invalid")
return value
def parse_notification(payload: Any) -> Notification:
if not isinstance(payload, dict):
raise ContractError("invalid_notification")
if set(payload) - set(NOTIFICATION_FIELDS):
raise ContractError("unknown_field")
if set(NOTIFICATION_FIELDS) - set(payload):
raise ContractError("missing_field")
scan_secrets(payload)
if any("://" in text for text in _strings(payload)):
raise ContractError("notification_has_link")
return Notification(
memo_id=_memo_id(payload["memo_id"]),
question=_one_question(payload["question"]),
memo_class=_slug(payload["memo_class"], "memo_class"),
submitter=_slug(payload["submitter"], "submitter"),
)
def build_envelope(
*,
decision_id: str,
memo: Mapping,
packet: list,
memo_class: str,
review_group: str,
approval_intent: Mapping,
drafted_by: str | None = None,
resolves: str = "on-answer",
) -> dict:
if not isinstance(memo, Mapping) or not isinstance(memo.get("id"), str):
raise ContractError("invalid_memo")
envelope: dict[str, Any] = {
"kind": KIND,
"version": VERSION,
"idempotency_key": stable_idempotency_key(decision_id, memo["id"]),
"memo_class": memo_class,
"review_group": review_group,
"serves": {"hub_decision_id": decision_id, "resolves": resolves},
"approval_intent": dict(approval_intent),
"memo": dict(memo),
"packet": list(packet),
}
if drafted_by is not None:
envelope["drafted_by"] = drafted_by
return validate_envelope(envelope)
def validate_envelope(envelope: Any) -> dict:
if not isinstance(envelope, dict):
raise ContractError("invalid_envelope")
extra = set(envelope) - ENVELOPE_KEYS
if extra:
raise ContractError("unknown_field")
missing = ENVELOPE_REQUIRED - set(envelope)
if missing:
raise ContractError("missing_field")
if envelope["kind"] != KIND or type(envelope["version"]) is not int or envelope["version"] != VERSION:
raise ContractError("unsupported_kind")
scan_secrets(envelope)
_packet(envelope["packet"])
_bounded_tree(envelope)
encoded = canonical(envelope).encode("utf-8")
if len(encoded) > MAX_ENVELOPE_BYTES:
raise ContractError("envelope_too_large")
memo = envelope["memo"]
if not isinstance(memo, dict):
raise ContractError("invalid_memo")
memo_id = _memo_id(memo.get("id"))
if "question" in memo:
_one_question(memo["question"])
decision_id = _serves(envelope["serves"])
key = _text(envelope["idempotency_key"], pattern=_KEY_RE, code="idempotency_key_invalid")
if key != stable_idempotency_key(decision_id, memo_id):
raise ContractError("idempotency_key_not_stable")
_slug(envelope["memo_class"], "memo_class")
_slug(envelope["review_group"], "review_group")
drafted_by = envelope.get("drafted_by")
if drafted_by is not None:
_text(drafted_by, max_len=MAX_DRAFTED_BY, code="drafted_by_invalid")
_intent(envelope["approval_intent"])
if "submitter" in envelope or "actor" in envelope.get("approval_intent", {}):
raise ContractError("unknown_field")
return json.loads(canonical(envelope))
def submission_digest(envelope: Mapping) -> str:
body = {key: value for key, value in envelope.items() if key != "idempotency_key"}
return digest(body)
def bounded_receipt(body: Any, expected_memo_id: str) -> dict[str, str]:
if not isinstance(body, dict):
raise ContractError("receipt_incomplete")
memo_id = body.get("memo_id")
approval_id = body.get("approval_id")
binding = body.get("approval_binding_digest")
if memo_id != expected_memo_id or not isinstance(approval_id, str) or not _UUID_RE.match(approval_id):
raise ContractError("receipt_mismatch")
if not isinstance(binding, str) or not _SHA_RE.match(binding):
raise ContractError("receipt_incomplete")
return {
"memo_id": memo_id,
"approval_id": approval_id,
"approval_binding_digest": binding,
}
def _serves(serves: Any) -> str:
if not isinstance(serves, dict) or set(serves) != {"hub_decision_id", "resolves"}:
raise ContractError("invalid_serves")
if serves["resolves"] not in ("on-answer", "never"):
raise ContractError("unknown_enum")
return _uuid(serves["hub_decision_id"], "hub_decision_id")
def _intent(intent: Any) -> None:
if not isinstance(intent, dict) or set(intent) != INTENT_KEYS:
raise ContractError("invalid_intent")
for field in ("action", "principal", "purpose"):
_text(intent[field], max_len=400, code="invalid_intent")
target = intent["target"]
if not isinstance(target, dict) or set(target) != TARGET_KEYS:
raise ContractError("invalid_intent")
for field in TARGET_KEYS:
_text(target[field], max_len=200, code="invalid_intent")
def _packet(packet: Any) -> None:
if not isinstance(packet, list) or not packet or len(packet) > MAX_PACKET_ITEMS:
raise ContractError("packet_required" if not packet else "packet_unbounded")
seen: set[str] = set()
for item in packet:
if not isinstance(item, dict) or set(item) != PACKET_KEYS:
raise ContractError("invalid_packet")
if item["media_type"] not in MEDIA_TYPES:
raise ContractError("unsupported_media_type")
content = item["content"]
if not isinstance(content, str) or len(content.encode("utf-8")) > MAX_PACKET_BYTES:
raise ContractError("packet_too_large")
item_id = _text(item["item_id"], max_len=200, code="invalid_packet")
if item_id in seen:
raise ContractError("duplicate_packet_item")
seen.add(item_id)
def _one_question(value: Any) -> str:
text = _text(value, max_len=MAX_QUESTION, code="question_invalid")
if "\n" in text or "\r" in text or "://" in text:
raise ContractError("question_invalid")
if text.count("?") != 1 or not text.endswith("?"):
raise ContractError("question_not_one_question")
return text
def _memo_id(value: Any) -> str:
return _text(value, pattern=_MEMO_ID_RE, code="memo_id_invalid")
def _slug(value: Any, code: str) -> str:
return _text(value, pattern=_SLUG_RE, code=code + "_invalid")
def _uuid(value: Any, code: str) -> str:
return _text(value, pattern=_UUID_RE, code=code + "_invalid")
def _text(value: Any, *, max_len: int = 128, pattern: re.Pattern[str] | None = None, code: str) -> str:
if not isinstance(value, str) or not value or len(value) > max_len:
raise ContractError(code)
if pattern is not None and not pattern.match(value):
raise ContractError(code)
return value
def _bounded_tree(value: Any) -> None:
total = 0
def walk(node: Any, key: str | None = None) -> None:
nonlocal total
if isinstance(node, str):
size = len(node.encode("utf-8"))
if size > MAX_PACKET_BYTES:
raise ContractError("field_too_large")
if key == "question":
_one_question(node)
total += size
elif isinstance(node, dict):
for child_key, child in node.items():
if not isinstance(child_key, str):
raise ContractError("invalid_type")
walk(child_key, None)
walk(child, child_key)
elif isinstance(node, list):
for child in node:
walk(child, key)
elif type(node) in (int, float) or node is None:
return
else:
raise ContractError("invalid_type")
if total > MAX_ENVELOPE_BYTES:
raise ContractError("envelope_too_large")
walk(value)
def _strings(value: Any):
if isinstance(value, str):
yield value
elif isinstance(value, dict):
for key, child in value.items():
if isinstance(key, str):
yield key
yield from _strings(child)
elif isinstance(value, list):
for child in value:
yield from _strings(child)