From dc1329a2e09862e1594e72b8dca31db8127b606c Mon Sep 17 00:00:00 2001 From: Paul Fidika Date: Fri, 7 Aug 2026 03:16:25 -0600 Subject: [PATCH] pgw#1008: the arm gate compares WHO THE CELL WAS FOR, not just who signed it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Twin of th#1657 (merged: tensorhub c2175b44 + fb23fa33). Makes this repo's own §6.14 note actionable — "adoption authority must be tied to the producing identity, not merely to a matching key". THE DEFECT, in this repo's own code. `owning_endpoint_id` has ridden the hub-signed receipt since pgw#709 and was decoded straight into `Receipt` (receipts.py:93, populated :248) — and compared against nothing. `verify_delivered_artifact` checked signature -> digest -> integral size -> embedded metadata -> packed cell key, and armed. A valid signature was read as "this cell is genuine" instead of "the hub signed this cell for THIS pod", so endpoint A's cell dlopen()ed on endpoint B's pods in another org. We were already paying for the attestation and throwing away the field that made it a trust decision. Discovery did not save us either: `aot_cells._discover_inner` lists the SHARED platform repo root/family- and filters on the DECLARE contract — not on endpoint, not on org, not even on equality with this pod's own computed key. The only producer property any layer checked was th#1643's cloud tier, which is a statement about the HARDWARE. THE RULE, verbatim from Paul's ruling: a cell must have come from THIS endpoint, or from a publisher the platform vouches for. - `Receipt` gains the th#1657 v2 claims `publisher_tier` + `publisher_org_id`. - `refuse_untrusted_publisher` runs LAST in `verify_delivered_artifact`: after the signature (a tier means nothing until the claims are proven) and before the return (the caller's next act is to arm and load native code). Typed class `publisher_untrusted` on the existing `cell_receipt_refused` event, so it is countable beside every other reject class (pgw#824). - This pod's identity comes from `cell_read_endpoint_id` on the hub-issued worker JWT (th#1335 grant + th#1657) — not config, not an env var. The payload is decoded WITHOUT signature verification on purpose: it is our own bearer token, not an input, and the hub verifies it on every call. The RECEIPT is the untrusted thing, and it is verified before the comparison. - Every route to "wider" is closed: the tier normalizes to `org` unless it is exactly `platform` (no third value, no unknown branch); an org receipt naming no endpoint is adoptable by nobody; a pod that cannot name itself is narrowed to platform-tier only; and RECEIPT_VERSION moves to cell-receipt-v2 so a v1 receipt is REFUSED, not read as a v2 one with the trust fields missing. RED-VERIFIED. With the one call stubbed out, 9 of the 12 new tests fail — including "a genuine, correctly-signed, un-revoked receipt for ANOTHER endpoint" arming. The 3 that still pass are exactly the controls (our own cell arms; platform tier arms; a v1 receipt is refused by version), which is the shape that proves the suite is not passing by refusing everything. The 25 pre-existing receipt tests were updated to mint v2 fixtures with a MATCHING org-tier publisher, so the new gate is live in every one of them rather than only in the cases that name it. Gates: mypy (235 files clean), ruff, http-timeout guard, unreached-surface guard, config-read guard (74 pairs, no new env conditional), full suite 3448 passed / 37 skipped / 1 xfailed. Nothing in the fleet changes behaviour today — every endpoint is currently platform-authored, so every cell publishes at platform tier and arms as before. That is exactly why it is cheap now. --- changelog.d/pgw1008.md | 37 +++++++ src/gen_worker/receipts.py | 121 +++++++++++++++++++++- tests/test_receipts_pgw709.py | 189 +++++++++++++++++++++++++++++++++- 3 files changed, 341 insertions(+), 6 deletions(-) create mode 100644 changelog.d/pgw1008.md diff --git a/changelog.d/pgw1008.md b/changelog.d/pgw1008.md new file mode 100644 index 00000000..e6564196 --- /dev/null +++ b/changelog.d/pgw1008.md @@ -0,0 +1,37 @@ +- **pgw#1008 (SECURITY, twin of th#1657): the arm gate verified WHO SIGNED a + cell and never WHO IT WAS FOR.** `owning_endpoint_id` has ridden the + hub-signed cell receipt since pgw#709 and was decoded straight into + `Receipt` — and then compared against nothing. So `verify_delivered_artifact` + checked the signature, the digest, the integral size and the packed cell key, + and armed. A valid signature was read as *"this cell is genuine"* rather than + *"the hub signed this cell for THIS pod"*, and endpoint A's cell `dlopen`ed on + endpoint B's pods, in another org. The SDK was already paying for the + attestation and discarding the one field that made it a trust decision. +- **The rule is Paul's ruling verbatim: a cell must have come from THIS + endpoint, or from a publisher the platform vouches for.** `Receipt` gains the + th#1657 v2 claims `publisher_tier` and `publisher_org_id`, and + `refuse_untrusted_publisher` runs LAST in `verify_delivered_artifact` — + after the signature, because a tier is only meaningful once the claims are + proven, and before the return, because the caller's next act is to arm the + cell and load native code. Typed refusal class: `publisher_untrusted`, on the + existing `cell_receipt_refused` wire event, so it is countable beside every + other reject class (pgw#824). +- **This pod's identity comes from the credential the hub issued it**, not from + config and not from an env var: `cell_read_endpoint_id`, which th#1657 stamps + on the th#1335 cell-read grant. The worker JWT payload is decoded WITHOUT + verifying its signature, deliberately — it is our own bearer token, not an + input, and the hub verifies it on every call. What must not be trusted is the + RECEIPT, and that one is signature-verified before it reaches the comparison. +- **Every route to "wider" is closed.** `publisher_tier` normalizes to `org` + unless it is exactly `platform` — no third value, no `unknown` branch, and + `PLATFORM`/`platform-ish`/absent all land on the narrower rule (§4.24 point 4: + absence must be explicit). An org-tier receipt naming no endpoint is adoptable + by nobody. A pod that cannot name its own endpoint is narrowed to + platform-tier cells, never widened. And `RECEIPT_VERSION` moves to + `cell-receipt-v2` so a v1 receipt is REFUSED rather than read as a v2 one with + the trust fields missing — the `omitempty` collapse that silently deletes a + boundary. +- **Nothing in the fleet changes behaviour today**, which is why it is cheap + now: every endpoint is currently platform-authored, so every cell publishes at + `platform` tier and arms exactly as before. The boundary has to exist before + that stops being true, not after. diff --git a/src/gen_worker/receipts.py b/src/gen_worker/receipts.py index effb71aa..4d672bc2 100644 --- a/src/gen_worker/receipts.py +++ b/src/gen_worker/receipts.py @@ -62,7 +62,20 @@ logger = logging.getLogger(__name__) -RECEIPT_VERSION = "cell-receipt-v1" +# th#1657 bumped v1 -> v2: the claim set gained `publisher_tier` and +# `publisher_org_id`, and they are LOAD-BEARING at the arm gate below. A v1 +# receipt must not be read as a v2 one with the trust fields missing, so the +# version check refuses it outright rather than defaulting them. +RECEIPT_VERSION = "cell-receipt-v2" + +# Publisher tiers (th#1657). `platform` means the platform vouches for the +# publishing org's endpoint code, so the cell is adoptable fleet-wide within its +# family. Anything else — including an absent or unrecognised value — is `org`, +# and an org-scoped cell is adoptable only by pods of the endpoint that minted +# it. There is deliberately no third value and no "unknown" branch: every +# unparseable tier must land on the NARROWER rule. +CELL_PUBLISHER_TIER_PLATFORM = "platform" +CELL_PUBLISHER_TIER_ORG = "org" # The algorithms this worker can actually recompute from local bytes. A # receipt naming anything else is refused, never assumed. ARTIFACT_DIGEST_ALGORITHMS = ("sha256",) @@ -92,6 +105,9 @@ class Receipt: axes: Dict[str, str] owning_endpoint_id: str publisher: str + # th#1657: the publisher-trust boundary, inside the signature. + publisher_tier: str + publisher_org_id: str snapshot_digest: str artifact_path: str # Canonical, ALGORITHM-TAGGED (":"). Never bare hex. @@ -150,6 +166,92 @@ def _b64url_decode(segment: str) -> bytes: raise ReceiptError("receipt_malformed", f"base64url decode failed: {exc}") from exc +# -- th#1657 publisher trust ------------------------------------------------ + + +def _normalize_publisher_tier(raw: object) -> str: + """Anything that is not exactly ``platform`` is ``org``. + + No error return and no third value on purpose: a caller forced to branch on + an error eventually gets the branch wrong, and every wrong branch here ends + in ``dlopen``. + """ + if str(raw or "").strip() == CELL_PUBLISHER_TIER_PLATFORM: + return CELL_PUBLISHER_TIER_PLATFORM + return CELL_PUBLISHER_TIER_ORG + + +def _self_endpoint_id(cfg: "_Config") -> str: + """The endpoint THIS pod serves, read out of the hub-issued worker JWT. + + The hub stamps ``cell_read_endpoint_id`` on the cell-read grant (th#1335 + + th#1657) precisely so both ends of the exchange can name the viewer. Reading + it here means the pod's identity comes from the credential the hub issued + for this pod — not from config, not from an env var, and not from anything + the cell being adopted can influence. + + The payload is decoded WITHOUT verifying the signature, and that is correct: + this is our OWN bearer token, not an input. A worker that forged its own + credential would be attacking itself, and the hub verifies it on every call + anyway. What must not be trusted is the RECEIPT, and that one is + signature-verified before it reaches the comparison. + """ + try: + token = str(cfg.worker_jwt() or "").strip() + except Exception: # noqa: BLE001 — a credential provider that raises is not an identity + return "" + parts = token.split(".") + if len(parts) != 3: + return "" + try: + payload = json.loads(_b64url_decode(parts[1]).decode("utf-8")) + except (ReceiptError, ValueError, UnicodeDecodeError): + return "" + if not isinstance(payload, dict): + return "" + return str(payload.get("cell_read_endpoint_id") or "").strip() + + +def refuse_untrusted_publisher(receipt: Receipt, self_endpoint_id: str) -> None: + """Raise unless this pod may adopt ``receipt``'s cell (th#1657). + + THE RULE, and it is Paul's ruling verbatim: a cell must have come from THIS + endpoint, or from a publisher the platform vouches for. + + THREAT: cross-tenant native-code execution. The artifact is a ``.so`` this + process is about to ``dlopen``. Nothing else prevents it — the digest proves + the bytes are the ones the hub signed, not that the hub meant them for US; + the cell key proves nothing at all, because the hub cannot verify + artifact-to-graph correspondence without recompiling; and the hub's own + listing filter (th#1657 hub half) is the only thing standing between us and + another org's cell on the ONE path that goes through a listing. This runs on + every path, and it is the last check before the load. + + THE FIELD WAS ALREADY HERE. ``owning_endpoint_id`` has ridden the signed + receipt since pgw#709 and was decoded into ``Receipt`` and then compared + against nothing — so a valid signature was read as "this cell is genuine" + and armed on whatever pod happened to fetch it. We were paying for the + attestation and discarding the field that made it a trust decision. + """ + if receipt.publisher_tier == CELL_PUBLISHER_TIER_PLATFORM: + return + owner = str(receipt.owning_endpoint_id or "").strip() + mine = str(self_endpoint_id or "").strip() + if not owner: + raise ReceiptError( + "publisher_untrusted", + "org-tier receipt names no owning endpoint, so no pod may adopt it") + if not mine: + raise ReceiptError( + "publisher_untrusted", + "this pod cannot name its own endpoint (no cell_read_endpoint_id on " + "the worker credential), so it may adopt platform-tier cells only") + if owner != mine: + raise ReceiptError( + "publisher_untrusted", + f"cell was minted for endpoint {owner} and this pod serves {mine}") + + def _rsa_key_from_jwk(jwk: Mapping[str, object]) -> Optional[rsa.RSAPublicKey]: if str(jwk.get("kty") or "") != "RSA": return None @@ -247,6 +349,8 @@ def verify_receipt_jws(jws: str, keys: Mapping[str, rsa.RSAPublicKey]) -> Receip axes=axes, owning_endpoint_id=str(payload.get("owning_endpoint_id") or ""), publisher=str(payload.get("publisher") or ""), + publisher_tier=_normalize_publisher_tier(payload.get("publisher_tier")), + publisher_org_id=str(payload.get("publisher_org_id") or ""), snapshot_digest=str(payload.get("snapshot_digest") or ""), artifact_path=str(artifact.get("path") or ""), artifact_digest=canonical_artifact_digest(str(artifact.get("digest") or "")), @@ -426,8 +530,14 @@ def verify_delivered_artifact(artifact: Path, family: str) -> Receipt: Chain of trust: receipt signature (hub key via JWKS) -> local bytes (the receipt's OWN algorithm + integral size) -> embedded metadata (inside the digested bytes) -> ``meta.cell_key == receipt.cell_key`` -> + **the PUBLISHER** (th#1657: platform tier, or this pod's own endpoint) -> the runtime's own computed key (enforced downstream by the th#883 selection brain). + + The publisher link is last because it is the only one that asks a question + about US rather than about the bytes, and it is the one that was missing: + every other link proved the artifact was the one the hub signed, and none + of them asked whether the hub signed it for this pod. """ with _LOCK: cfg = _CONFIG @@ -487,6 +597,12 @@ def verify_delivered_artifact(artifact: Path, family: str) -> Receipt: "cell_revoked", f"key={receipt.cell_key} snapshot={receipt.snapshot_digest} is recalled") + # th#1657, LAST: who published this, and may we run it. Deliberately after + # the signature — the tier is only meaningful once the claims are proven — + # and deliberately before the return, because the caller's next act is to + # arm the cell and dlopen it. + refuse_untrusted_publisher(receipt, _self_endpoint_id(cfg)) + return receipt @@ -532,6 +648,8 @@ def gate_delivered_artifact(artifact: Path, family: str) -> bool: __all__ = [ "ARTIFACT_DIGEST_ALGORITHMS", + "CELL_PUBLISHER_TIER_ORG", + "CELL_PUBLISHER_TIER_PLATFORM", "Receipt", "ReceiptError", "artifact_digests", @@ -539,6 +657,7 @@ def gate_delivered_artifact(artifact: Path, family: str) -> bool: "configure", "configured", "gate_delivered_artifact", + "refuse_untrusted_publisher", "reset", "verify_delivered_artifact", "verify_receipt_jws", diff --git a/tests/test_receipts_pgw709.py b/tests/test_receipts_pgw709.py index e7665543..b42fe3f2 100644 --- a/tests/test_receipts_pgw709.py +++ b/tests/test_receipts_pgw709.py @@ -29,6 +29,13 @@ FAMILY = "sdxl" CELL_KEY = "ck1-0123456789abcdef0123456789abcdef0123456789abcdef01234567" SNAPSHOT = "snapdigest-abc123" +# th#1657: the endpoint the test pod serves, and the one every fixture receipt +# is minted FOR unless a test deliberately mints it for someone else. Keeping +# the default a matching ORG-tier pair means the publisher gate is live in every +# case below, not just the ones that name it. +SELF_ENDPOINT = "3e0f8f7a-1111-2222-3333-444455556666" +OTHER_ENDPOINT = "9c1d2e3f-9999-8888-7777-666655554444" +SELF_ORG = "11111111-2222-3333-4444-555555555555" B3_HEX = "ab12cd34" * 8 SHA_HEX = "12ab34cd" * 8 @@ -74,12 +81,15 @@ def make_claims( else: artifact["digest"] = artifact_digest claims: Dict[str, Any] = { - "crv": "cell-receipt-v1", + "crv": "cell-receipt-v2", "family": FAMILY, "cell_key": CELL_KEY, "axes": {"sku": "rtx-4090", "image_digest": "sha256:feed", "gen_worker": "0.75.1"}, - "owning_endpoint_id": "3e0f8f7a-1111-2222-3333-444455556666", + "owning_endpoint_id": SELF_ENDPOINT, "publisher": "selfmint:worker=w1:pod=p1:release=r1", + # th#1657 publisher trust, inside the signature. + "publisher_tier": "org", + "publisher_org_id": SELF_ORG, "snapshot_digest": SNAPSHOT, "artifact": artifact, "manifest_digest": "sha256:aa", @@ -162,7 +172,7 @@ def test_wrong_key_refused(self, rsa_key: rsa.RSAPrivateKey) -> None: assert exc.value.reason == "receipt_signature_invalid" def test_wrong_version_refused(self, rsa_key: rsa.RSAPrivateKey, pub_map: Dict[str, rsa.RSAPublicKey]) -> None: - jws = sign_receipt(rsa_key, make_claims("sha256:" + SHA_HEX, 4096, crv="cell-receipt-v0")) + jws = sign_receipt(rsa_key, make_claims("sha256:" + SHA_HEX, 4096, crv="cell-receipt-v1")) with pytest.raises(receipts.ReceiptError) as exc: receipts.verify_receipt_jws(jws, pub_map) assert exc.value.reason == "receipt_version_unsupported" @@ -269,8 +279,24 @@ def hub(rsa_key: rsa.RSAPrivateKey) -> Iterator[HubStub]: receipts.reset() -def _configure(stub: HubStub) -> None: - receipts.configure(base_url=stub.base_url, worker_jwt=lambda: "test-worker-jwt") +def worker_jwt_for(endpoint_id: str) -> str: + """A hub-shaped worker credential naming the endpoint this pod serves. + + th#1657: the pod's own identity comes from the `cell_read_endpoint_id` the + hub stamps on the cell-read grant (th#1335), so the test builds the same + thing. The signature is never checked — this is our OWN bearer token, not an + input — so an unsigned third segment is faithful to what the gate reads. + """ + header = _b64url(json.dumps({"alg": "RS256", "typ": "JWT"}).encode()) + payload = _b64url(json.dumps({ + "sub": "worker-1", "cell_read_endpoint_id": endpoint_id, + }).encode()) + return header + "." + payload + ".not-checked-here" + + +def _configure(stub: HubStub, *, endpoint_id: str = SELF_ENDPOINT) -> None: + receipts.configure( + base_url=stub.base_url, worker_jwt=lambda: worker_jwt_for(endpoint_id)) class TestGateDeliveredArtifact: @@ -515,3 +541,156 @@ def test_digests_are_computed_in_one_pass(self, tmp_path: Path) -> None: assert set(got) == set(receipts.ARTIFACT_DIGEST_ALGORITHMS) == {"sha256"} raw = artifact.read_bytes() assert got["sha256"] == hashlib.sha256(raw).hexdigest() + + +# --------------------------------------------------------------------------- +# th#1657 — the publisher trust boundary at the arm gate (pgw#1008) +# --------------------------------------------------------------------------- + + +class TestPublisherTrustTh1657: + """A cell must have come from THIS endpoint, or from a publisher the + platform vouches for. + + THREAT: cross-tenant native-code execution. The artifact is a `.so` this + process is about to dlopen. Every other link in the chain proves the bytes + are the ones the hub signed; none of them asks whether the hub signed them + FOR THIS POD. `owning_endpoint_id` has ridden the signed receipt since + pgw#709 and was decoded into `Receipt` and compared against nothing. + + The first test is the RED one: before pgw#1008 it ARMS. + """ + + def test_another_endpoints_org_cell_is_refused( + self, tmp_path: Path, hub: HubStub + ) -> None: + """A genuine, correctly-signed, un-revoked receipt — for someone else. + + Nothing about this artifact is malformed. The signature verifies, the + digest matches, the size matches, the packed key matches, the family + matches, the pair is not revoked. It is simply not ours, and before + pgw#1008 that made no difference at all. + """ + artifact = make_artifact(tmp_path) + hub.serve_receipt_for( + artifact, owning_endpoint_id=OTHER_ENDPOINT, publisher_tier="org") + _configure(hub, endpoint_id=SELF_ENDPOINT) + + with pytest.raises(receipts.ReceiptError) as excinfo: + receipts.verify_delivered_artifact(artifact, FAMILY) + assert excinfo.value.reason == "publisher_untrusted" + assert OTHER_ENDPOINT in str(excinfo.value) + # And the arm hook refuses rather than raising into the boot. + assert receipts.gate_delivered_artifact(artifact, FAMILY) is False + + def test_our_own_org_cell_arms(self, tmp_path: Path, hub: HubStub) -> None: + """THE CONTROL. Without it, a gate that refused everything would pass + every other assertion in this class.""" + artifact = make_artifact(tmp_path) + hub.serve_receipt_for( + artifact, owning_endpoint_id=SELF_ENDPOINT, publisher_tier="org") + _configure(hub, endpoint_id=SELF_ENDPOINT) + + receipt = receipts.verify_delivered_artifact(artifact, FAMILY) + assert receipt.owning_endpoint_id == SELF_ENDPOINT + assert receipt.publisher_tier == "org" + assert receipts.gate_delivered_artifact(artifact, FAMILY) is True + + def test_platform_tier_arms_anywhere(self, tmp_path: Path, hub: HubStub) -> None: + """Platform-tier is the escape hatch the fleet actually runs on: the + platform authored that code and already runs it everywhere.""" + artifact = make_artifact(tmp_path) + hub.serve_receipt_for( + artifact, owning_endpoint_id=OTHER_ENDPOINT, publisher_tier="platform") + _configure(hub, endpoint_id=SELF_ENDPOINT) + + receipt = receipts.verify_delivered_artifact(artifact, FAMILY) + assert receipt.publisher_tier == "platform" + assert receipts.gate_delivered_artifact(artifact, FAMILY) is True + + @pytest.mark.parametrize("tier", [None, "", "platform-ish", "PLATFORM", "org"]) + def test_only_exactly_platform_widens( + self, tmp_path: Path, hub: HubStub, tier: object + ) -> None: + """§4.24 point 4: absence must be explicit. An unset, mis-cased or + invented tier must land on the NARROWER rule, never the wider one — a + receipt is a permanent statement, so there is no reader-side leniency + to lean on.""" + artifact = make_artifact(tmp_path) + overrides: Dict[str, Any] = {"owning_endpoint_id": OTHER_ENDPOINT} + if tier is not None: + overrides["publisher_tier"] = tier + else: + overrides["publisher_tier"] = None + hub.serve_receipt_for(artifact, **overrides) + _configure(hub, endpoint_id=SELF_ENDPOINT) + + with pytest.raises(receipts.ReceiptError) as excinfo: + receipts.verify_delivered_artifact(artifact, FAMILY) + assert excinfo.value.reason == "publisher_untrusted" + + def test_pod_that_cannot_name_itself_gets_platform_only( + self, tmp_path: Path, hub: HubStub + ) -> None: + """A worker credential with no `cell_read_endpoint_id` (a hub too old + for th#1657, or a grant that could not resolve one) narrows this pod to + platform-tier cells. It does NOT widen it — an identity we cannot + establish is not an identity that matches everyone.""" + artifact = make_artifact(tmp_path) + hub.serve_receipt_for( + artifact, owning_endpoint_id=SELF_ENDPOINT, publisher_tier="org") + receipts.configure(base_url=hub.base_url, worker_jwt=lambda: "not-a-jwt") + + with pytest.raises(receipts.ReceiptError) as excinfo: + receipts.verify_delivered_artifact(artifact, FAMILY) + assert excinfo.value.reason == "publisher_untrusted" + assert "cannot name its own endpoint" in str(excinfo.value) + + def test_org_receipt_naming_no_endpoint_is_adoptable_by_nobody( + self, tmp_path: Path, hub: HubStub + ) -> None: + artifact = make_artifact(tmp_path) + hub.serve_receipt_for( + artifact, owning_endpoint_id="", publisher_tier="org") + _configure(hub, endpoint_id=SELF_ENDPOINT) + + with pytest.raises(receipts.ReceiptError) as excinfo: + receipts.verify_delivered_artifact(artifact, FAMILY) + assert excinfo.value.reason == "publisher_untrusted" + + def test_v1_receipt_is_refused_not_defaulted( + self, tmp_path: Path, hub: HubStub + ) -> None: + """The trust fields are load-bearing, so a receipt minted before they + existed must not be read as a v2 one with them missing. That is the + `omitempty` collapse §4.24 point 4 names, and here it would silently + delete the boundary.""" + artifact = make_artifact(tmp_path) + hub.serve_receipt_for(artifact, crv="cell-receipt-v1") + _configure(hub, endpoint_id=SELF_ENDPOINT) + + with pytest.raises(receipts.ReceiptError) as excinfo: + receipts.verify_delivered_artifact(artifact, FAMILY) + assert excinfo.value.reason == "receipt_version_unsupported" + + def test_the_refusal_reaches_the_wire( + self, tmp_path: Path, hub: HubStub, monkeypatch: pytest.MonkeyPatch + ) -> None: + """pgw#824/pgw#999: a refusal nobody can count is a refusal nobody can + act on. The class must reach the activity event, not just the log.""" + events: List[Tuple[str, str, str]] = [] + monkeypatch.setattr( + receipts.activity_mod, "emit_event", + lambda kind, detail, phase="", **_: events.append((kind, detail, phase))) + + artifact = make_artifact(tmp_path) + hub.serve_receipt_for( + artifact, owning_endpoint_id=OTHER_ENDPOINT, publisher_tier="org") + _configure(hub, endpoint_id=SELF_ENDPOINT) + + assert receipts.gate_delivered_artifact(artifact, FAMILY) is False + assert events, "the refusal never reached the wire" + kind, detail, phase = events[-1] + assert kind == "cell_receipt_refused" + assert phase == "publisher_untrusted" + assert FAMILY in detail