diff --git a/.github/workflows/outbox-delivery-receipt-quality.yml b/.github/workflows/outbox-delivery-receipt-quality.yml new file mode 100644 index 000000000..15971ca24 --- /dev/null +++ b/.github/workflows/outbox-delivery-receipt-quality.yml @@ -0,0 +1,57 @@ +name: Outbox Delivery Receipt Quality + +on: + pull_request: + branches: + - develop + paths: + - "packages/outbox-delivery-receipt/**" + - ".github/requirements/foundation-test.txt" + - ".github/workflows/outbox-delivery-receipt-quality.yml" + - "docs/adr/0151-governed-external-delivery-receipt.md" + - "docs/traceability/outbox-delivery-receipt.md" + - "docs/doctoring/outbox-delivery-receipt-references.md" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: outbox-delivery-receipt-quality-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + unit: + name: External delivery receipt contract and 100% coverage + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout exact candidate + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + - name: Prove exact candidate checkout + env: + ORGMETRA_EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: test "$(git rev-parse HEAD)" = "$ORGMETRA_EXPECTED_HEAD_SHA" + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14" + check-latest: false + - name: Install reviewed test toolchain + run: | + python -m pip install --require-hashes --no-deps --only-binary=:all: -r .github/requirements/foundation-test.txt + python -m pip check + - name: Compile delivery receipt package + run: python -m compileall -q packages/outbox-delivery-receipt/src packages/outbox-delivery-receipt/tests + - name: Test external delivery receipt with exact statement and branch coverage + env: + PYTHONPATH: packages/outbox-delivery-receipt/src + COVERAGE_FILE: /tmp/orgmetra-outbox-delivery-receipt.coverage + run: python -m pytest -c packages/outbox-delivery-receipt/pyproject.toml packages/outbox-delivery-receipt/tests + - name: Require clean checkout + run: | + git diff --exit-code + test -z "$(git status --porcelain)" diff --git a/docs/adr/0151-governed-external-delivery-receipt.md b/docs/adr/0151-governed-external-delivery-receipt.md new file mode 100644 index 000000000..7ef1274cd --- /dev/null +++ b/docs/adr/0151-governed-external-delivery-receipt.md @@ -0,0 +1,77 @@ +# ADR 0151: Govern external transport delivery receipt evidence before outbox completion + +- **Status:** Proposed — active PR #151; not protected-main truth +- **Date:** 2026-08-29 +- **Owners:** Orgmetra integration/audit boundary +- **Decision scope:** Evidence needed between an external transport response and an + authoritative Orgmetra outbox completion transaction + +## Context + +Protected `develop` already persists immutable audit events and mutable outbox delivery +state. `complete_outbox_delivery(...)` correctly requires a current tenant-scoped live +lease, but the protected function does not itself require evidence from the external +transport that handled the attempt. + +ADR 0006 identified external delivery receipts as subsequent work. No open Orgmetra PR was +found that owned a generic external outbox receipt contract; HR export PR #120 owns a +different one-time export-egress receipt boundary, and retry-policy PR #82 owns scheduling, +not transport acknowledgement. + +## Decision + +Add a standalone package that constructs a value-minimized +`ExternalDeliveryReceiptEvidence` and verifies exact-attempt correlation. + +The evidence binds tenant/outbox/audit/target/attempt, a descriptive transport-provider +code, a host-normalized opaque receipt reference, SHA-256 of the exact external receipt +artifact, provider-reported delivery time, host observation time, and evidence version. + +External transport evidence remains explicitly untrusted and carries +`not_authorized_to_mutate_delivery_state`. It excludes raw provider responses and protected +HR values. Canonical export revalidates every trust-bearing field, including instances +created through copy or low-level tuple construction, so those construction paths cannot +bypass fixed safety-state, shape, chronology, or identifier invariants. Separately +constructed receipts remain untrusted and still require authoritative exact-attempt and +artifact reconciliation. + +Trust-bearing primitive values are accepted only as their exact built-in Python types, +not caller-defined subclasses whose equality or serialization behavior can be overridden. +Caller-owned aware datetimes are normalized once during construction into detached, +built-in UTC `datetime` values before the evidence object retains them. Later changes to a +caller-owned timezone provider therefore cannot rewrite the canonical JSON or digest. +Canonical export rejects low-level reconstructed evidence unless both stored timestamps +are already those frozen built-in UTC values. Exact-attempt verification likewise accepts +only the exact `ExternalDeliveryReceiptEvidence` type so a subclass cannot override the +returned digest or other trust behavior. + +## Why not modify the outbox migration here + +Open Orgmetra stacks already carry many provisional database migrations. Adding another +durable migration before the evidence contract is reviewed would increase collision and +restack risk. This slice establishes the package/API evidence boundary first. A subsequent +authoritative persistence change may bind the canonical receipt digest into outbox +completion after dependency order permits; it must not backfill or rewrite immutable audit +history. + +## Consequences + +- A caller can correlate a normalized external receipt to one exact current outbox attempt + before authoritative completion. +- A receipt cannot by itself authorize `complete_outbox_delivery(...)`. +- Provider raw payloads, addresses, credentials, HR content, and employment decisions stay + out of governance evidence. +- Receipt replay across retry attempts fails exact-attempt reconciliation. +- Mutable timezone providers and behavior-overriding primitive subclasses cannot remain + embedded in canonical receipt evidence after construction. +- The contract remains independently extractable as an MSA/API boundary. + +## Cryptographic and time references + +SHA-256 is used only as deterministic artifact-correlation evidence, not as a signature or +proof of provider identity. NIST continues to list SHA-2/SHA-256 under FIPS 180-4 while a +revision of FIPS 180-4 has been announced. UTC `Z` rendering follows the RFC 3339 timestamp +form with RFC 9557's update to the semantics of `Z`; Orgmetra uses it here simply as a +canonical zero-offset representation. + +See `docs/doctoring/outbox-delivery-receipt-references.md`. diff --git a/docs/doctoring/outbox-delivery-receipt-references.md b/docs/doctoring/outbox-delivery-receipt-references.md new file mode 100644 index 000000000..ea98b1385 --- /dev/null +++ b/docs/doctoring/outbox-delivery-receipt-references.md @@ -0,0 +1,25 @@ +# External delivery receipt — primary references + +These references support only the narrow cryptographic/time representation decisions in +PR #151. They do not imply certification, provider authenticity, delivery guarantees, or +employment-law compliance. + +## APA 7 references + +National Institute of Standards and Technology. (2015). *Secure Hash Standard (SHS)* +(FIPS PUB 180-4). U.S. Department of Commerce. https://doi.org/10.6028/NIST.FIPS.180-4 + +National Institute of Standards and Technology. (2023, March 7). *Decision to revise FIPS +180-4, Secure Hash Standard (SHS).* https://csrc.nist.gov/News/2023/decision-to-revise-fips-180-4 + +Sharma, U., & Bormann, C. (2024). *Date and time on the Internet: Timestamps with +additional information* (RFC 9557). RFC Editor. https://doi.org/10.17487/RFC9557 + +## Decision notes + +- SHA-256 is used for content correlation, not signing. NIST's current CAVP secure-hashing + material continues to list SHA-256 in the SHA-2 family under FIPS 180-4; NIST has also + announced that FIPS 180-4 will be revised. +- RFC 9557 updates RFC 3339's interpretation of the `Z` local-offset marker. Orgmetra uses + `Z` only to produce one deterministic UTC/zero-offset text representation for evidence + hashing; it does not encode a source time zone. diff --git a/docs/traceability/outbox-delivery-receipt.md b/docs/traceability/outbox-delivery-receipt.md new file mode 100644 index 000000000..30b1351c4 --- /dev/null +++ b/docs/traceability/outbox-delivery-receipt.md @@ -0,0 +1,34 @@ +# External delivery receipt traceability + +**State:** Active PR #151 only. Protected `develop` does not yet expose this package. + +| Requirement | Executable evidence | Production boundary | +| --- | --- | --- | +| Exact tenant/outbox/audit/target/attempt binding | `test_verifies_only_the_exact_outbox_attempt` | `verify_exact_delivery_attempt` | +| No HR payload, destination, or credential in the canonical evidence | `test_builds_value_minimized_untrusted_transport_evidence`; fixed-contract parametrization | `ExternalDeliveryReceiptEvidence` fixed safety fields | +| External receipt remains untrusted and non-authorizing | fixed-contract parametrization; hostile trust-state subclass regression | exact-type fixed-state validation | +| Opaque normalized receipt identity | receipt-reference parametrization | `_validate_receipt_reference` | +| Exact provider artifact correlation | digest parametrization | `_validate_digest`, `transport_receipt_digest` | +| Temporal evidence is detached from caller-owned timezone behavior and canonical UTC; observation cannot predate reported delivery | UTC precision, mutable-timezone, provider-failure, no-offset, and chronology regressions | `_freeze_timestamp`, `_canonical_timestamp`, `_validate_contract` | +| Trust-bearing text cannot retain behavior-overriding `str` subclasses | exact-attempt equality and fixed trust-state subclass regressions | exact built-in primitive validation | +| Receipt subclasses cannot override verification/digest behavior | `test_exact_attempt_verification_rejects_receipt_subclasses` | exact-type check in `verify_exact_delivery_attempt` | +| Retry replay cannot cross attempt boundaries | exact-attempt mismatch regression | `delivery_attempt_count` in reconciliation tuple | +| Copy/low-level reconstruction cannot bypass fixed safety/trust invariants | `test_copy_bypass_cannot_create_a_second_canonical_truth` | canonical export revalidation | +| Structural mutation is rejected | `test_evidence_is_structurally_immutable_after_construction` | tuple-backed evidence type | +| Exact owned statement/branch coverage | hosted `Outbox Delivery Receipt Quality` | pytest-cov gate at 100% | + +## Upstream protected-main truth + +- `database/migrations/0003_audit_outbox_persistence.sql` owns immutable audit events and + durable outbox state. +- `database/migrations/0005_outbox_delivery_finalization.sql` owns live-lease completion + and retry mutation. +- This PR does not change either migration and does not claim a durable receipt column. + +## Downstream acceptance + +Before any later durable receipt persistence or `delivered` transition is considered +commercial truth, the authoritative host must re-resolve the current tenant-scoped leased +attempt, verify the raw external artifact against the stored digest, preserve immutable +audit evidence, and pass the then-current exact-head migration/recovery/security/review +gates. diff --git a/packages/outbox-delivery-receipt/CHANGELOG.md b/packages/outbox-delivery-receipt/CHANGELOG.md new file mode 100644 index 000000000..0adad89b5 --- /dev/null +++ b/packages/outbox-delivery-receipt/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +## Unreleased + +- Define value-minimized external transport delivery receipt evidence. +- Bind receipts to an exact tenant/outbox/audit/target/attempt coordinate. +- Keep transport evidence untrusted and explicitly non-authorizing for delivery-state + mutation. +- Require canonical UTC chronology, opaque normalized receipt references, SHA-256 artifact + correlation, structural immutability, copy-bypass revalidation, and exact 100% owned + statement/branch coverage. +- Detach caller-owned timezone behavior into built-in UTC timestamps before evidence is + retained, reject behavior-overriding trust/identifier string subclasses, and reject + receipt subclasses at exact-attempt verification so canonical evidence and returned + digests cannot be rewritten through caller-controlled runtime behavior. diff --git a/packages/outbox-delivery-receipt/README.md b/packages/outbox-delivery-receipt/README.md new file mode 100644 index 000000000..b250c9ad1 --- /dev/null +++ b/packages/outbox-delivery-receipt/README.md @@ -0,0 +1,50 @@ +# Orgmetra external delivery receipt evidence + +This package gives Orgmetra a small, value-minimized evidence object for the moment an +external transport reports that one outbox attempt was delivered. + +It **does not** mark an outbox row delivered. A transport response is untrusted evidence. +The authoritative Orgmetra host must re-read the live tenant-scoped leased attempt, verify +the normalized receipt artifact, apply purpose-bound authorization, and persist its own +immutable audit/outbox evidence in the same governed completion transaction. + +## What the evidence binds + +`ExternalDeliveryReceiptEvidence` binds one exact: + +- tenant, outbox delivery, and audit event; +- delivery target and attempt number; +- transport provider code; +- host-normalized opaque `transport_receipt:` reference; +- SHA-256 digest of the exact external receipt artifact; +- provider-reported delivery instant and host observation instant; and +- evidence version. + +The canonical packet never carries the HR payload, destination address, credentials, +compensation, assessment/rating values, free-form model output, or an employment decision. + +## Safe next action + +Call `verify_exact_delivery_attempt(...)` only after resolving the authoritative current +outbox attempt. A successful match returns the canonical evidence digest for correlation; +it is still **not** permission to call `complete_outbox_delivery(...)`. The host must verify +the external receipt artifact against `transport_receipt_digest` and complete its normal +lease, authorization, audit, and persistence checks. + +## Integrity model + +The public evidence type is a tuple-backed immutable value. Ordinary mutation through +`setattr` or `object.__setattr__` fails. Canonical export also revalidates every +trust-bearing field, so copy helpers or low-level tuple construction cannot bypass the +fixed safety-state, shape, chronology, or identifier invariants. A separately constructed +receipt is still untrusted evidence and must independently match the authoritative exact +attempt plus the external receipt artifact before any governed completion can occur. + +This is an application evidence contract, not a digital-signature scheme. Durable +cross-process authenticity and retention belong to the authoritative persistence and +audit/outbox boundary. + +## Current integration status + +This package is proposed by PR #151. Until that PR integrates into protected `develop`, +it is active-PR truth, not a commercially available protected-main capability. diff --git a/packages/outbox-delivery-receipt/SECURITY.md b/packages/outbox-delivery-receipt/SECURITY.md new file mode 100644 index 000000000..e487b5326 --- /dev/null +++ b/packages/outbox-delivery-receipt/SECURITY.md @@ -0,0 +1,26 @@ +# Security and privacy boundary + +External transport receipts are attacker-controlled input until reconciled by Orgmetra. + +The contract therefore: + +- treats transport evidence as `untrusted_transport_evidence`; +- binds it to one tenant/outbox/audit/target/attempt coordinate; +- stores only a host-normalized opaque reference and SHA-256 digest, not raw provider + response bodies; +- excludes HR payloads, destinations, credentials, free-form text, compensation, ratings, + assessment outcomes, and model output; +- rejects noncanonical/sentinel UUID identities, malformed governance codes, non-UUIDv4 + normalized receipt references, invalid digests, unbounded attempt/version values, and + impossible observation chronology; +- revalidates trust-bearing fields on canonical export to catch copy/bypass-created + instances; and +- never grants authority to mutate `outbox_delivery_record`. + +A consumer must not interpret a provider-reported receipt as proof that the intended human +or system actually consumed the message. It is evidence that the configured transport +reported delivery for the correlated attempt. Downstream business semantics need their +own explicit acknowledgement contract. + +No secret, provider token, destination address, or raw transport payload belongs in this +evidence packet or routine logs. diff --git a/packages/outbox-delivery-receipt/pyproject.toml b/packages/outbox-delivery-receipt/pyproject.toml new file mode 100644 index 000000000..a4d4b1fc7 --- /dev/null +++ b/packages/outbox-delivery-receipt/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["setuptools>=69"] +build-backend = "setuptools.build_meta" + +[project] +name = "orgmetra-outbox-delivery-receipt" +version = "0.1.0" +description = "PII-minimized external transport delivery receipt evidence for Orgmetra outbox reconciliation." +requires-python = ">=3.12" + +[project.optional-dependencies] +test = ["pytest>=8.3", "pytest-cov>=5.0"] + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = [ + "--cov=orgmetra_outbox_delivery_receipt", + "--cov-branch", + "--cov-report=term-missing", + "--cov-fail-under=100", +] diff --git a/packages/outbox-delivery-receipt/src/orgmetra_outbox_delivery_receipt/__init__.py b/packages/outbox-delivery-receipt/src/orgmetra_outbox_delivery_receipt/__init__.py new file mode 100644 index 000000000..447f58939 --- /dev/null +++ b/packages/outbox-delivery-receipt/src/orgmetra_outbox_delivery_receipt/__init__.py @@ -0,0 +1,13 @@ +"""Public contract for Orgmetra external outbox delivery receipt evidence.""" + +from .receipt import ( + ExternalDeliveryReceiptEvidence, + build_external_delivery_receipt_evidence, + verify_exact_delivery_attempt, +) + +__all__ = [ + "ExternalDeliveryReceiptEvidence", + "build_external_delivery_receipt_evidence", + "verify_exact_delivery_attempt", +] diff --git a/packages/outbox-delivery-receipt/src/orgmetra_outbox_delivery_receipt/receipt.py b/packages/outbox-delivery-receipt/src/orgmetra_outbox_delivery_receipt/receipt.py new file mode 100644 index 000000000..7d2dd08c0 --- /dev/null +++ b/packages/outbox-delivery-receipt/src/orgmetra_outbox_delivery_receipt/receipt.py @@ -0,0 +1,398 @@ +"""Value-minimized external transport delivery receipt evidence for Orgmetra. + +The provider receipt is untrusted evidence, not delivery-state mutation authority. Raw +provider payloads, destinations, credentials, and HR values remain outside this packet. +The authoritative host must match this evidence to one live leased outbox attempt before +it can consider a separately governed completion transaction. +""" +from __future__ import annotations + +from collections import namedtuple +from datetime import datetime, timezone +from hashlib import sha256 +import json +import re +from uuid import UUID + +_CODE_PATTERN = re.compile(r"^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$") +_DIGEST_PATTERN = re.compile(r"^[0-9a-f]{64}$") +_RECEIPT_PREFIX = "transport_receipt" +_MAX_INT = 2_147_483_647 + +_DELIVERY_OUTCOME_CODE = "transport_reported_delivered" +_TRUST_STATE = "untrusted_transport_evidence" +_RECONCILIATION_STATE = "requires_exact_attempt_reconciliation" +_MUTATION_AUTHORITY = "not_authorized_to_mutate_delivery_state" +_NEXT_ACTION = ( + "Reconcile this normalized receipt to the exact live tenant/outbox/audit/target/attempt " + "under the authoritative Orgmetra lease and purpose-bound authorization boundary; verify " + "the external receipt artifact against transport_receipt_digest, then persist immutable " + "audit/outbox evidence atomically before marking delivery complete." +) + + +def _validate_operational_uuid(value: object, field_name: str) -> str: + """Return canonical built-in UUID text or fail closed.""" + if type(value) is not str: + raise ValueError(f"{field_name} must be canonical UUID text") + try: + parsed = UUID(value) + except ValueError as exc: + raise ValueError(f"{field_name} must be canonical UUID text") from exc + if str(parsed) != value or parsed.int in (0, (1 << 128) - 1): + raise ValueError(f"{field_name} must be a canonical operational UUID") + return value + + +def _validate_code(value: object, field_name: str) -> str: + """Return a bounded built-in two-or-more-word lower snake_case code.""" + if type(value) is not str or len(value) > 64 or not _CODE_PATTERN.fullmatch(value): + raise ValueError(f"{field_name} must be bounded two-or-more-word lower snake_case") + return value + + +def _validate_positive_int(value: object, field_name: str) -> int: + """Return a positive bounded integer while rejecting booleans.""" + if type(value) is not int or value < 1 or value > _MAX_INT: + raise ValueError(f"{field_name} must be an integer from 1 through {_MAX_INT}") + return value + + +def _validate_receipt_reference(value: object) -> str: + """Require a built-in Orgmetra-normalized opaque UUIDv4 receipt reference.""" + message = "transport_receipt_reference must be an opaque transport_receipt: UUIDv4 reference" + if type(value) is not str or len(value) > 160 or not value.startswith(f"{_RECEIPT_PREFIX}:"): + raise ValueError(message) + suffix = value.split(":", 1)[1] + try: + parsed = UUID(suffix) + except ValueError as exc: + raise ValueError(message) from exc + if str(parsed) != suffix or parsed.version != 4 or parsed.int in (0, (1 << 128) - 1): + raise ValueError(message) + return value + + +def _validate_digest(value: object) -> str: + """Require built-in lowercase SHA-256 evidence for the external receipt artifact.""" + if type(value) is not str or not _DIGEST_PATTERN.fullmatch(value): + raise ValueError("transport_receipt_digest must be lowercase SHA-256 hex") + return value + + +def _freeze_timestamp(value: object, field_name: str) -> datetime: + """Detach caller-owned timezone behavior into one built-in UTC datetime.""" + if type(value) is not datetime or value.tzinfo is None: + raise ValueError(f"{field_name} must be timezone-aware") + try: + if value.utcoffset() is None: + raise ValueError(f"{field_name} must be timezone-aware") + normalized = value.astimezone(timezone.utc) + except Exception as exc: + if isinstance(exc, ValueError) and str(exc) == f"{field_name} must be timezone-aware": + raise + raise ValueError(f"{field_name} must be safely normalizable to UTC") from exc + return datetime( + normalized.year, + normalized.month, + normalized.day, + normalized.hour, + normalized.minute, + normalized.second, + normalized.microsecond, + tzinfo=timezone.utc, + ) + + +def _canonical_timestamp(value: object, field_name: str) -> str: + """Return canonical text only for already-frozen built-in UTC evidence.""" + if type(value) is not datetime or value.tzinfo is not timezone.utc: + raise ValueError(f"{field_name} must be frozen built-in UTC datetime evidence") + return value.isoformat().replace("+00:00", "Z") + + +def _validate_contract( + *, + tenant_record_id: object, + outbox_delivery_record_id: object, + audit_event_record_id: object, + delivery_target_code: object, + delivery_attempt_count: object, + transport_provider_code: object, + transport_receipt_reference: object, + transport_receipt_digest: object, + transport_delivered_at: object, + observed_at: object, + evidence_version: object, + contains_hr_payload: object, + contains_destination: object, + contains_credentials: object, + delivery_outcome_code: object, + trust_state: object, + reconciliation_state: object, + mutation_authority: object, + next_action: object, +) -> tuple[str, str]: + """Revalidate every trust-bearing field, including copy/bypass-created instances.""" + _validate_operational_uuid(tenant_record_id, "tenant_record_id") + _validate_operational_uuid(outbox_delivery_record_id, "outbox_delivery_record_id") + _validate_operational_uuid(audit_event_record_id, "audit_event_record_id") + _validate_code(delivery_target_code, "delivery_target_code") + _validate_positive_int(delivery_attempt_count, "delivery_attempt_count") + _validate_code(transport_provider_code, "transport_provider_code") + _validate_receipt_reference(transport_receipt_reference) + _validate_digest(transport_receipt_digest) + transport_delivered_at_utc = _canonical_timestamp( + transport_delivered_at, "transport_delivered_at" + ) + observed_at_utc = _canonical_timestamp(observed_at, "observed_at") + if observed_at < transport_delivered_at: + raise ValueError("observed_at cannot precede transport_delivered_at") + _validate_positive_int(evidence_version, "evidence_version") + + fixed_values = { + "contains_hr_payload": (contains_hr_payload, False), + "contains_destination": (contains_destination, False), + "contains_credentials": (contains_credentials, False), + "delivery_outcome_code": (delivery_outcome_code, _DELIVERY_OUTCOME_CODE), + "trust_state": (trust_state, _TRUST_STATE), + "reconciliation_state": (reconciliation_state, _RECONCILIATION_STATE), + "mutation_authority": (mutation_authority, _MUTATION_AUTHORITY), + "next_action": (next_action, _NEXT_ACTION), + } + for field_name, (actual, required) in fixed_values.items(): + if type(actual) is not type(required) or actual != required: + raise ValueError(f"{field_name} must remain fixed by the governed receipt contract") + return transport_delivered_at_utc, observed_at_utc + + +_BaseReceipt = namedtuple( + "_BaseReceipt", + [ + "tenant_record_id", + "outbox_delivery_record_id", + "audit_event_record_id", + "delivery_target_code", + "delivery_attempt_count", + "transport_provider_code", + "transport_receipt_reference", + "transport_receipt_digest", + "transport_delivered_at", + "observed_at", + "evidence_version", + "contains_hr_payload", + "contains_destination", + "contains_credentials", + "delivery_outcome_code", + "trust_state", + "reconciliation_state", + "mutation_authority", + "next_action", + ], +) + + +class ExternalDeliveryReceiptEvidence(_BaseReceipt): + """Structurally immutable evidence that an external transport reported delivery.""" + + __slots__ = () + + def __new__( + cls, + *, + tenant_record_id: str, + outbox_delivery_record_id: str, + audit_event_record_id: str, + delivery_target_code: str, + delivery_attempt_count: int, + transport_provider_code: str, + transport_receipt_reference: str, + transport_receipt_digest: str, + transport_delivered_at: datetime, + observed_at: datetime, + evidence_version: int = 1, + contains_hr_payload: bool = False, + contains_destination: bool = False, + contains_credentials: bool = False, + delivery_outcome_code: str = _DELIVERY_OUTCOME_CODE, + trust_state: str = _TRUST_STATE, + reconciliation_state: str = _RECONCILIATION_STATE, + mutation_authority: str = _MUTATION_AUTHORITY, + next_action: str = _NEXT_ACTION, + ) -> "ExternalDeliveryReceiptEvidence": + frozen_transport_delivered_at = _freeze_timestamp( + transport_delivered_at, "transport_delivered_at" + ) + frozen_observed_at = _freeze_timestamp(observed_at, "observed_at") + _validate_contract( + tenant_record_id=tenant_record_id, + outbox_delivery_record_id=outbox_delivery_record_id, + audit_event_record_id=audit_event_record_id, + delivery_target_code=delivery_target_code, + delivery_attempt_count=delivery_attempt_count, + transport_provider_code=transport_provider_code, + transport_receipt_reference=transport_receipt_reference, + transport_receipt_digest=transport_receipt_digest, + transport_delivered_at=frozen_transport_delivered_at, + observed_at=frozen_observed_at, + evidence_version=evidence_version, + contains_hr_payload=contains_hr_payload, + contains_destination=contains_destination, + contains_credentials=contains_credentials, + delivery_outcome_code=delivery_outcome_code, + trust_state=trust_state, + reconciliation_state=reconciliation_state, + mutation_authority=mutation_authority, + next_action=next_action, + ) + + instance = super().__new__( + cls, + tenant_record_id, + outbox_delivery_record_id, + audit_event_record_id, + delivery_target_code, + delivery_attempt_count, + transport_provider_code, + transport_receipt_reference, + transport_receipt_digest, + frozen_transport_delivered_at, + frozen_observed_at, + evidence_version, + contains_hr_payload, + contains_destination, + contains_credentials, + delivery_outcome_code, + trust_state, + reconciliation_state, + mutation_authority, + next_action, + ) + return instance + + def __repr__(self) -> str: + """Redact correlation identifiers from routine logs.""" + return "ExternalDeliveryReceiptEvidence()" + + @property + def transport_delivered_at_utc(self) -> str: + """Return the provider-reported delivery instant in canonical UTC text.""" + return _canonical_timestamp(self.transport_delivered_at, "transport_delivered_at") + + @property + def observed_at_utc(self) -> str: + """Return the host observation instant in canonical UTC text.""" + return _canonical_timestamp(self.observed_at, "observed_at") + + def canonical_json(self) -> str: + """Return deterministic value-minimized JSON for immutable audit correlation.""" + transport_delivered_at_utc, observed_at_utc = _validate_contract( + tenant_record_id=self.tenant_record_id, + outbox_delivery_record_id=self.outbox_delivery_record_id, + audit_event_record_id=self.audit_event_record_id, + delivery_target_code=self.delivery_target_code, + delivery_attempt_count=self.delivery_attempt_count, + transport_provider_code=self.transport_provider_code, + transport_receipt_reference=self.transport_receipt_reference, + transport_receipt_digest=self.transport_receipt_digest, + transport_delivered_at=self.transport_delivered_at, + observed_at=self.observed_at, + evidence_version=self.evidence_version, + contains_hr_payload=self.contains_hr_payload, + contains_destination=self.contains_destination, + contains_credentials=self.contains_credentials, + delivery_outcome_code=self.delivery_outcome_code, + trust_state=self.trust_state, + reconciliation_state=self.reconciliation_state, + mutation_authority=self.mutation_authority, + next_action=self.next_action, + ) + payload = { + "audit_event_record_id": self.audit_event_record_id, + "contains_credentials": self.contains_credentials, + "contains_destination": self.contains_destination, + "contains_hr_payload": self.contains_hr_payload, + "delivery_attempt_count": self.delivery_attempt_count, + "delivery_outcome_code": self.delivery_outcome_code, + "delivery_target_code": self.delivery_target_code, + "evidence_version": self.evidence_version, + "mutation_authority": self.mutation_authority, + "next_action": self.next_action, + "observed_at": observed_at_utc, + "outbox_delivery_record_id": self.outbox_delivery_record_id, + "reconciliation_state": self.reconciliation_state, + "tenant_record_id": self.tenant_record_id, + "transport_delivered_at": transport_delivered_at_utc, + "transport_provider_code": self.transport_provider_code, + "transport_receipt_digest": self.transport_receipt_digest, + "transport_receipt_reference": self.transport_receipt_reference, + "trust_state": self.trust_state, + } + return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + + def sha256_digest(self) -> str: + """Return SHA-256 over the exact canonical UTF-8 receipt evidence.""" + return sha256(self.canonical_json().encode("utf-8")).hexdigest() + + +def build_external_delivery_receipt_evidence( + *, + tenant_record_id: str, + outbox_delivery_record_id: str, + audit_event_record_id: str, + delivery_target_code: str, + delivery_attempt_count: int, + transport_provider_code: str, + transport_receipt_reference: str, + transport_receipt_digest: str, + transport_delivered_at: datetime, + observed_at: datetime, + evidence_version: int = 1, +) -> ExternalDeliveryReceiptEvidence: + """Build one untrusted normalized receipt for later exact-attempt reconciliation.""" + return ExternalDeliveryReceiptEvidence( + tenant_record_id=tenant_record_id, + outbox_delivery_record_id=outbox_delivery_record_id, + audit_event_record_id=audit_event_record_id, + delivery_target_code=delivery_target_code, + delivery_attempt_count=delivery_attempt_count, + transport_provider_code=transport_provider_code, + transport_receipt_reference=transport_receipt_reference, + transport_receipt_digest=transport_receipt_digest, + transport_delivered_at=transport_delivered_at, + observed_at=observed_at, + evidence_version=evidence_version, + ) + + +def verify_exact_delivery_attempt( + evidence: ExternalDeliveryReceiptEvidence, + *, + tenant_record_id: str, + outbox_delivery_record_id: str, + audit_event_record_id: str, + delivery_target_code: str, + delivery_attempt_count: int, +) -> str: + """Fail closed unless receipt evidence matches the exact authoritative attempt scope.""" + if type(evidence) is not ExternalDeliveryReceiptEvidence: + raise TypeError("evidence must be ExternalDeliveryReceiptEvidence") + + evidence_digest = evidence.sha256_digest() + expected = ( + _validate_operational_uuid(tenant_record_id, "tenant_record_id"), + _validate_operational_uuid(outbox_delivery_record_id, "outbox_delivery_record_id"), + _validate_operational_uuid(audit_event_record_id, "audit_event_record_id"), + _validate_code(delivery_target_code, "delivery_target_code"), + _validate_positive_int(delivery_attempt_count, "delivery_attempt_count"), + ) + actual = ( + evidence.tenant_record_id, + evidence.outbox_delivery_record_id, + evidence.audit_event_record_id, + evidence.delivery_target_code, + evidence.delivery_attempt_count, + ) + if actual != expected: + raise ValueError("receipt evidence does not match the exact outbox delivery attempt") + return evidence_digest diff --git a/packages/outbox-delivery-receipt/tests/test_receipt.py b/packages/outbox-delivery-receipt/tests/test_receipt.py new file mode 100644 index 000000000..8f584a2e5 --- /dev/null +++ b/packages/outbox-delivery-receipt/tests/test_receipt.py @@ -0,0 +1,298 @@ +from __future__ import annotations + +from datetime import datetime, timedelta, timezone, tzinfo +from uuid import UUID, uuid4 + +import pytest + +from orgmetra_outbox_delivery_receipt import ( + ExternalDeliveryReceiptEvidence, + build_external_delivery_receipt_evidence, + verify_exact_delivery_attempt, +) + + +class _MutableTimezone(tzinfo): + def __init__(self, offset: timedelta) -> None: + self.offset = offset + + def utcoffset(self, dt: datetime | None) -> timedelta: + return self.offset + + def dst(self, dt: datetime | None) -> timedelta: + return timedelta(0) + + +class _EqualityForgingStr(str): + def __eq__(self, other: object) -> bool: + return True + + def __ne__(self, other: object) -> bool: + return False + + __hash__ = str.__hash__ + + +def _uuid() -> str: + return str(uuid4()) + + +def _reference(prefix: str) -> str: + return f"{prefix}:{uuid4()}" + + +def _kwargs() -> dict[str, object]: + return { + "tenant_record_id": _uuid(), + "outbox_delivery_record_id": _uuid(), + "audit_event_record_id": _uuid(), + "delivery_target_code": "naruon_calendar", + "delivery_attempt_count": 2, + "transport_provider_code": "calendar_gateway", + "transport_receipt_reference": _reference("transport_receipt"), + "transport_receipt_digest": "a" * 64, + "transport_delivered_at": datetime(2026, 8, 29, 1, 2, 3, 456789, tzinfo=timezone.utc), + "observed_at": datetime(2026, 8, 29, 1, 2, 4, tzinfo=timezone.utc), + "evidence_version": 1, + } + + +def test_builds_value_minimized_untrusted_transport_evidence() -> None: + evidence = build_external_delivery_receipt_evidence(**_kwargs()) + + assert evidence.contains_hr_payload is False + assert evidence.contains_destination is False + assert evidence.contains_credentials is False + assert evidence.delivery_outcome_code == "transport_reported_delivered" + assert evidence.trust_state == "untrusted_transport_evidence" + assert evidence.reconciliation_state == "requires_exact_attempt_reconciliation" + assert evidence.mutation_authority == "not_authorized_to_mutate_delivery_state" + assert "reconcile" in evidence.next_action.lower() + assert repr(evidence) == "ExternalDeliveryReceiptEvidence()" + + payload = evidence.canonical_json() + assert '"contains_hr_payload":false' in payload + assert '"transport_receipt_digest":"' + "a" * 64 + '"' in payload + assert "destination" in payload + assert evidence.sha256_digest() == evidence.sha256_digest() + assert len(evidence.sha256_digest()) == 64 + + +def test_canonicalizes_aware_timestamps_to_utc_without_losing_precision() -> None: + values = _kwargs() + values["transport_delivered_at"] = datetime( + 2026, 8, 29, 10, 2, 3, 456789, tzinfo=timezone(timedelta(hours=9)) + ) + values["observed_at"] = datetime( + 2026, 8, 29, 10, 2, 4, 123, tzinfo=timezone(timedelta(hours=9)) + ) + evidence = build_external_delivery_receipt_evidence(**values) + + assert evidence.transport_delivered_at_utc == "2026-08-29T01:02:03.456789Z" + assert evidence.observed_at_utc == "2026-08-29T01:02:04.000123Z" + + +def test_freezes_caller_owned_timezone_before_evidence_is_retained() -> None: + mutable_timezone = _MutableTimezone(timedelta(hours=9)) + values = _kwargs() + values["transport_delivered_at"] = datetime( + 2026, 8, 29, 10, 2, 3, 456789, tzinfo=mutable_timezone + ) + evidence = build_external_delivery_receipt_evidence(**values) + original_json = evidence.canonical_json() + original_digest = evidence.sha256_digest() + + mutable_timezone.offset = timedelta(0) + + assert evidence.transport_delivered_at.tzinfo is timezone.utc + assert evidence.canonical_json() == original_json + assert evidence.sha256_digest() == original_digest + + +def test_rejects_string_subclass_that_can_forge_exact_attempt_equality() -> None: + values = _kwargs() + values["delivery_target_code"] = _EqualityForgingStr("naruon_calendar") + + with pytest.raises(ValueError, match="delivery_target_code"): + build_external_delivery_receipt_evidence(**values) + + +def test_rejects_string_subclass_that_can_forge_fixed_trust_state() -> None: + values = _kwargs() + values["trust_state"] = _EqualityForgingStr("trusted_transport_evidence") + + with pytest.raises(ValueError, match="trust_state"): + ExternalDeliveryReceiptEvidence(**values) + + +def test_verifies_only_the_exact_outbox_attempt() -> None: + evidence = build_external_delivery_receipt_evidence(**_kwargs()) + + digest = verify_exact_delivery_attempt( + evidence, + tenant_record_id=evidence.tenant_record_id, + outbox_delivery_record_id=evidence.outbox_delivery_record_id, + audit_event_record_id=evidence.audit_event_record_id, + delivery_target_code=evidence.delivery_target_code, + delivery_attempt_count=evidence.delivery_attempt_count, + ) + assert digest == evidence.sha256_digest() + + with pytest.raises(ValueError, match="exact outbox delivery attempt"): + verify_exact_delivery_attempt( + evidence, + tenant_record_id=evidence.tenant_record_id, + outbox_delivery_record_id=evidence.outbox_delivery_record_id, + audit_event_record_id=evidence.audit_event_record_id, + delivery_target_code=evidence.delivery_target_code, + delivery_attempt_count=evidence.delivery_attempt_count + 1, + ) + + with pytest.raises(TypeError, match="ExternalDeliveryReceiptEvidence"): + verify_exact_delivery_attempt( + object(), + tenant_record_id=evidence.tenant_record_id, + outbox_delivery_record_id=evidence.outbox_delivery_record_id, + audit_event_record_id=evidence.audit_event_record_id, + delivery_target_code=evidence.delivery_target_code, + delivery_attempt_count=evidence.delivery_attempt_count, + ) + + +@pytest.mark.parametrize( + ("field_name", "bad_value"), + [ + ("tenant_record_id", "not-a-uuid"), + ("outbox_delivery_record_id", "00000000-0000-0000-0000-000000000000"), + ("audit_event_record_id", "ffffffff-ffff-ffff-ffff-ffffffffffff"), + ("tenant_record_id", UUID("12345678-1234-5678-9234-567812345678")), + ], +) +def test_rejects_non_operational_or_noncanonical_uuid_identity( + field_name: str, bad_value: object +) -> None: + values = _kwargs() + values[field_name] = bad_value + with pytest.raises(ValueError, match=field_name): + build_external_delivery_receipt_evidence(**values) + + +@pytest.mark.parametrize( + ("field_name", "bad_value"), + [ + ("delivery_target_code", "calendar"), + ("delivery_target_code", "Calendar_Gateway"), + ("delivery_target_code", 3), + ("delivery_target_code", "a_" + "b" * 64), + ("transport_provider_code", "provider"), + ("transport_provider_code", "bad-provider"), + ], +) +def test_rejects_unbounded_or_free_form_codes(field_name: str, bad_value: object) -> None: + values = _kwargs() + values[field_name] = bad_value + with pytest.raises(ValueError, match=field_name): + build_external_delivery_receipt_evidence(**values) + + +@pytest.mark.parametrize( + "bad_value", + [ + "receipt:550e8400-e29b-41d4-a716-446655440000", + "transport_receipt:not-a-uuid", + "transport_receipt:550e8400-e29b-11d4-a716-446655440000", + 5, + "transport_receipt:" + "x" * 200, + ], +) +def test_requires_host_normalized_opaque_transport_receipt_reference(bad_value: object) -> None: + values = _kwargs() + values["transport_receipt_reference"] = bad_value + with pytest.raises(ValueError, match="transport_receipt_reference"): + build_external_delivery_receipt_evidence(**values) + + +@pytest.mark.parametrize("bad_value", ["A" * 64, "a" * 63, 7]) +def test_requires_lowercase_sha256_receipt_digest(bad_value: object) -> None: + values = _kwargs() + values["transport_receipt_digest"] = bad_value + with pytest.raises(ValueError, match="transport_receipt_digest"): + build_external_delivery_receipt_evidence(**values) + + +@pytest.mark.parametrize("bad_value", [True, 0, 2_147_483_648]) +def test_requires_positive_bounded_delivery_attempt_count(bad_value: object) -> None: + values = _kwargs() + values["delivery_attempt_count"] = bad_value + with pytest.raises(ValueError, match="delivery_attempt_count"): + build_external_delivery_receipt_evidence(**values) + + +@pytest.mark.parametrize("bad_value", [True, 0, 2_147_483_648]) +def test_requires_positive_bounded_evidence_version(bad_value: object) -> None: + values = _kwargs() + values["evidence_version"] = bad_value + with pytest.raises(ValueError, match="evidence_version"): + build_external_delivery_receipt_evidence(**values) + + +@pytest.mark.parametrize( + ("field_name", "bad_value"), + [ + ("transport_delivered_at", "2026-08-29T01:02:03Z"), + ("transport_delivered_at", datetime(2026, 8, 29, 1, 2, 3)), + ("observed_at", datetime(2026, 8, 29, 1, 2, 4)), + ], +) +def test_requires_timezone_aware_datetime_evidence(field_name: str, bad_value: object) -> None: + values = _kwargs() + values[field_name] = bad_value + with pytest.raises(ValueError, match=field_name): + build_external_delivery_receipt_evidence(**values) + + +def test_rejects_receipt_observed_before_reported_delivery() -> None: + values = _kwargs() + values["observed_at"] = values["transport_delivered_at"] - timedelta(microseconds=1) + with pytest.raises(ValueError, match="observed_at"): + build_external_delivery_receipt_evidence(**values) + + +@pytest.mark.parametrize( + ("field_name", "bad_value"), + [ + ("contains_hr_payload", True), + ("contains_destination", True), + ("contains_credentials", True), + ("delivery_outcome_code", "delivered"), + ("trust_state", "trusted"), + ("reconciliation_state", "reconciled"), + ("mutation_authority", "authorized"), + ("next_action", "Mark delivered."), + ], +) +def test_fixed_safety_contract_cannot_be_overridden(field_name: str, bad_value: object) -> None: + values = _kwargs() + values[field_name] = bad_value + with pytest.raises(ValueError): + ExternalDeliveryReceiptEvidence(**values) + + +def test_evidence_is_structurally_immutable_after_construction() -> None: + evidence = build_external_delivery_receipt_evidence(**_kwargs()) + with pytest.raises(AttributeError): + object.__setattr__(evidence, "delivery_attempt_count", 99) + + +def test_copy_bypass_cannot_create_a_second_canonical_truth() -> None: + evidence = build_external_delivery_receipt_evidence(**_kwargs()) + + replaced = evidence._replace(trust_state="trusted_transport_evidence") + with pytest.raises(ValueError, match="trust_state"): + replaced.canonical_json() + + raw_values = list(evidence) + raw_values[11] = True + reconstructed = tuple.__new__(ExternalDeliveryReceiptEvidence, tuple(raw_values)) + with pytest.raises(ValueError, match="contains_hr_payload"): + reconstructed.sha256_digest() diff --git a/packages/outbox-delivery-receipt/tests/test_runtime_integrity.py b/packages/outbox-delivery-receipt/tests/test_runtime_integrity.py new file mode 100644 index 000000000..e7709c3e8 --- /dev/null +++ b/packages/outbox-delivery-receipt/tests/test_runtime_integrity.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +from datetime import datetime, timedelta, timezone, tzinfo +from uuid import uuid4 + +import pytest + +from orgmetra_outbox_delivery_receipt import ( + ExternalDeliveryReceiptEvidence, + build_external_delivery_receipt_evidence, + verify_exact_delivery_attempt, +) + + +class _FailingTimezone(tzinfo): + def utcoffset(self, dt: datetime | None) -> timedelta: + raise RuntimeError("hostile timezone provider") + + def dst(self, dt: datetime | None) -> timedelta: + return timedelta(0) + + +class _NoOffsetTimezone(tzinfo): + def utcoffset(self, dt: datetime | None) -> None: + return None + + def dst(self, dt: datetime | None) -> None: + return None + + +class _ExplosiveEquality: + def __eq__(self, other: object) -> bool: + raise RuntimeError("untrusted evidence equality executed") + + +def _kwargs() -> dict[str, object]: + return { + "tenant_record_id": str(uuid4()), + "outbox_delivery_record_id": str(uuid4()), + "audit_event_record_id": str(uuid4()), + "delivery_target_code": "naruon_calendar", + "delivery_attempt_count": 2, + "transport_provider_code": "calendar_gateway", + "transport_receipt_reference": f"transport_receipt:{uuid4()}", + "transport_receipt_digest": "a" * 64, + "transport_delivered_at": datetime(2026, 8, 29, 1, 2, 3, tzinfo=timezone.utc), + "observed_at": datetime(2026, 8, 29, 1, 2, 4, tzinfo=timezone.utc), + "evidence_version": 1, + } + + +def test_timezone_provider_exception_fails_closed_as_value_error() -> None: + values = _kwargs() + values["transport_delivered_at"] = datetime( + 2026, 8, 29, 1, 2, 3, tzinfo=_FailingTimezone() + ) + + with pytest.raises(ValueError, match="transport_delivered_at"): + build_external_delivery_receipt_evidence(**values) + + +def test_timezone_provider_without_offset_fails_closed() -> None: + values = _kwargs() + values["transport_delivered_at"] = datetime( + 2026, 8, 29, 1, 2, 3, tzinfo=_NoOffsetTimezone() + ) + + with pytest.raises(ValueError, match="transport_delivered_at"): + build_external_delivery_receipt_evidence(**values) + + +def test_low_level_reconstruction_with_nonfrozen_timestamp_fails_closed() -> None: + evidence = build_external_delivery_receipt_evidence(**_kwargs()) + raw_values = list(evidence) + raw_values[8] = datetime( + 2026, 8, 29, 10, 2, 3, tzinfo=timezone(timedelta(hours=9)) + ) + reconstructed = tuple.__new__(ExternalDeliveryReceiptEvidence, tuple(raw_values)) + + with pytest.raises(ValueError, match="transport_delivered_at"): + reconstructed.canonical_json() + + +def test_exact_attempt_verification_rejects_receipt_subclasses() -> None: + evidence = build_external_delivery_receipt_evidence(**_kwargs()) + + class _ForgedReceipt(ExternalDeliveryReceiptEvidence): + __slots__ = () + + def sha256_digest(self) -> str: + return "f" * 64 + + forged = tuple.__new__(_ForgedReceipt, tuple(evidence)) + + with pytest.raises(TypeError, match="ExternalDeliveryReceiptEvidence"): + verify_exact_delivery_attempt( + forged, + tenant_record_id=evidence.tenant_record_id, + outbox_delivery_record_id=evidence.outbox_delivery_record_id, + audit_event_record_id=evidence.audit_event_record_id, + delivery_target_code=evidence.delivery_target_code, + delivery_attempt_count=evidence.delivery_attempt_count, + ) + + +def test_exact_attempt_verification_validates_evidence_before_scope_comparison() -> None: + evidence = build_external_delivery_receipt_evidence(**_kwargs()) + raw_values = list(evidence) + raw_values[0] = _ExplosiveEquality() + reconstructed = tuple.__new__(ExternalDeliveryReceiptEvidence, tuple(raw_values)) + + with pytest.raises(ValueError, match="tenant_record_id"): + verify_exact_delivery_attempt( + reconstructed, + tenant_record_id=evidence.tenant_record_id, + outbox_delivery_record_id=evidence.outbox_delivery_record_id, + audit_event_record_id=evidence.audit_event_record_id, + delivery_target_code=evidence.delivery_target_code, + delivery_attempt_count=evidence.delivery_attempt_count, + )