[verified] Handle delivery acknowledgements and read-only recovery
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
"""Fail-closed decoding for one Gitea API delivery attempt.
|
||||
|
||||
Unexpected/malformed acknowledgements can follow a committed mutation: callers
|
||||
must not blindly retry. They may perform caller-initiated read-only reconciliation.
|
||||
"""
|
||||
import json
|
||||
import ssl
|
||||
from urllib.error import HTTPError, URLError
|
||||
|
||||
MAX_RESPONSE_BYTES = 1024 * 1024
|
||||
|
||||
|
||||
class DeliveryResponseError(Exception):
|
||||
"""Sanitized response failure; message never includes server content."""
|
||||
|
||||
|
||||
class InvalidExpectationError(DeliveryResponseError, ValueError):
|
||||
"""The caller supplied an invalid endpoint-specific status expectation."""
|
||||
|
||||
|
||||
class RejectedResponseError(DeliveryResponseError):
|
||||
"""Known auth/redirect or other non-success rejection; do not retry blindly."""
|
||||
|
||||
|
||||
class AcknowledgementUncertainError(DeliveryResponseError):
|
||||
"""Mutation may have committed; do not retry; reconcile read-only if needed."""
|
||||
|
||||
|
||||
def _validate_expected_status(expected_status):
|
||||
if (isinstance(expected_status, bool) or not isinstance(expected_status, int)
|
||||
or expected_status < 200 or expected_status > 299):
|
||||
raise InvalidExpectationError("expected_status must be an explicit success status integer")
|
||||
|
||||
|
||||
def _reject_constant(_value):
|
||||
raise ValueError("non-standard JSON constant")
|
||||
|
||||
|
||||
def decode_response(status, body, *, expected_status):
|
||||
"""Require the exact expected status and a bounded JSON object body."""
|
||||
_validate_expected_status(expected_status)
|
||||
if isinstance(status, bool) or not isinstance(status, int):
|
||||
raise AcknowledgementUncertainError("invalid response status; do not retry; reconcile read-only")
|
||||
if 300 <= status <= 399 or status in (401, 403):
|
||||
raise RejectedResponseError("request rejected or redirected; do not retry blindly")
|
||||
if status != expected_status:
|
||||
raise AcknowledgementUncertainError("unexpected response status; do not retry; reconcile read-only")
|
||||
if not isinstance(body, (bytes, bytearray)) or len(body) > MAX_RESPONSE_BYTES:
|
||||
raise AcknowledgementUncertainError("invalid or oversized response; do not retry; reconcile read-only")
|
||||
try:
|
||||
value = json.loads(bytes(body).decode("utf-8"), parse_constant=_reject_constant)
|
||||
except (UnicodeError, ValueError, RecursionError):
|
||||
raise AcknowledgementUncertainError("invalid JSON response; do not retry; reconcile read-only") from None
|
||||
if not isinstance(value, dict):
|
||||
raise AcknowledgementUncertainError("response JSON must be an object; do not retry; reconcile read-only")
|
||||
return value
|
||||
|
||||
|
||||
def deliver_once(send, *, expected_status):
|
||||
"""Validate expectation, invoke injected sender exactly once, decode its (status, bytes)."""
|
||||
_validate_expected_status(expected_status)
|
||||
try:
|
||||
result = send()
|
||||
except (RejectedResponseError, ssl.SSLCertVerificationError):
|
||||
raise RejectedResponseError("transport security rejected; do not retry blindly") from None
|
||||
except HTTPError as error:
|
||||
if 300 <= error.code <= 399 or error.code in (401, 403):
|
||||
raise RejectedResponseError("request rejected or redirected; do not retry blindly") from None
|
||||
raise AcknowledgementUncertainError("HTTP acknowledgement uncertain; do not retry; reconcile read-only") from None
|
||||
except URLError as error:
|
||||
if isinstance(error.reason, ssl.SSLCertVerificationError):
|
||||
raise RejectedResponseError("TLS verification rejected; do not retry blindly") from None
|
||||
raise AcknowledgementUncertainError("transport failure; do not retry; reconcile read-only") from None
|
||||
except Exception:
|
||||
raise AcknowledgementUncertainError("transport failure; do not retry; reconcile read-only") from None
|
||||
try:
|
||||
status, body = result
|
||||
except Exception:
|
||||
raise AcknowledgementUncertainError("invalid transport response; do not retry; reconcile read-only") from None
|
||||
return decode_response(status, body, expected_status=expected_status)
|
||||
+39
@@ -88,3 +88,42 @@ def deliver_implementation(adapter: Any, *, head: str, declaration: Any) -> dict
|
||||
readback = adapter.read_pr(pr_number)
|
||||
bounded_pr = _validate_readback(readback, pr_number, head)
|
||||
return {"remote_head": remote_head, "pr": bounded_pr}
|
||||
|
||||
|
||||
def reconcile_implementation(adapter: Any, *, head: str, number: int,
|
||||
declaration: Any) -> dict[str, Any]:
|
||||
"""Explicit read-only recovery after uncertain write acknowledgement.
|
||||
|
||||
The trusted main-owned adapter must freshly verify identity/scope, exact
|
||||
intended body, protected Issue snapshot, and complete files/commits via
|
||||
verify_reconciliation(head, number). Only literal True accepts that proof.
|
||||
These callbacks MUST be read-only. This is not a transport sandbox, an
|
||||
automatic exception handler, or permission to retry a write.
|
||||
"""
|
||||
_validate_head(head)
|
||||
_validate_pr_number(number)
|
||||
for name in ("read_remote_head", "read_pr", "verify_reconciliation"):
|
||||
callback = None
|
||||
try:
|
||||
callback = getattr(adapter, name)
|
||||
except Exception:
|
||||
_reject("trusted reconciliation adapter required")
|
||||
if not callable(callback):
|
||||
_reject("trusted reconciliation adapter required")
|
||||
|
||||
remote_head = _delivery_adoption.deliver(
|
||||
adapter.read_remote_head, head=head, declaration=declaration
|
||||
)
|
||||
if type(remote_head) is not str or remote_head != head:
|
||||
_reject("remote head mismatch")
|
||||
readback = _delivery_adoption.deliver(
|
||||
lambda: adapter.read_pr(number), head=head, declaration=declaration
|
||||
)
|
||||
bounded_pr = _validate_readback(readback, number, head)
|
||||
verified = _delivery_adoption.deliver(
|
||||
lambda: adapter.verify_reconciliation(head, number),
|
||||
head=head, declaration=declaration
|
||||
)
|
||||
if verified is not True:
|
||||
_reject("reconciliation intent or protected state mismatch")
|
||||
return {"remote_head": remote_head, "pr": bounded_pr}
|
||||
|
||||
Reference in New Issue
Block a user