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
382 lines
13 KiB
Python
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)
|