From fc814f9e05599d67b4efd1e217b3873f3f946766 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 19:51:15 +0900 Subject: [PATCH 1/5] test(evidence): require builder-issued decision records --- tests/test_decision_evidence_factory_only.py | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/test_decision_evidence_factory_only.py diff --git a/tests/test_decision_evidence_factory_only.py b/tests/test_decision_evidence_factory_only.py new file mode 100644 index 0000000..ab5f8c5 --- /dev/null +++ b/tests/test_decision_evidence_factory_only.py @@ -0,0 +1,25 @@ +"""Regression tests for builder-issued decision evidence.""" + +from __future__ import annotations + +import pytest + +from egressweave import EgressDecisionEvidence + + +def test_decision_evidence_rejects_direct_public_construction() -> None: + """Require decision evidence to come from the validating builder.""" + with pytest.raises( + TypeError, + match="EgressDecisionEvidence objects must come from the evidence builder", + ): + EgressDecisionEvidence( + schema_version="egressweave.decision-evidence.v1", + authority="api.example.com:443", + allowed_methods=("GET",), + address_count=1, + ipv4_address_count=1, + ipv6_address_count=0, + policy_fingerprint="0" * 64, + decision_fingerprint="1" * 64, + ) From 4f0306c7f6839bfebd5aaad5453c3e820b71937e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 19:54:43 +0900 Subject: [PATCH 2/5] fix(evidence): issue successful records only through builder --- src/egressweave/decision_evidence.py | 53 ++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/src/egressweave/decision_evidence.py b/src/egressweave/decision_evidence.py index b6edddb..068663f 100644 --- a/src/egressweave/decision_evidence.py +++ b/src/egressweave/decision_evidence.py @@ -91,14 +91,15 @@ def _policy_fingerprint(policy: EgressPolicy) -> str: return _sha256_canonical_json(payload) -@dataclass(frozen=True) +@dataclass(frozen=True, init=False) class EgressDecisionEvidence: """Immutable audit evidence for one successfully authorized authority. - The record intentionally omits the request path and every resolved address. - Fingerprints are deterministic correlation values, not signatures or message - authentication codes, and must not be treated as proof against a process - that can execute arbitrary Python code. + The supported issuance path is :func:`build_egress_decision_evidence`, which + revalidates signed URL state before creating a record. The record deliberately + omits the request path and every resolved address. Fingerprints are deterministic + correlation values, not signatures or message authentication codes, and must not + be treated as proof against a process that can execute arbitrary Python code. """ schema_version: str @@ -110,6 +111,22 @@ class EgressDecisionEvidence: policy_fingerprint: str decision_fingerprint: str + def __init__( + self, + schema_version: str, + authority: str, + allowed_methods: tuple[str, ...], + address_count: int, + ipv4_address_count: int, + ipv6_address_count: int, + policy_fingerprint: str, + decision_fingerprint: str, + ) -> None: + """Reject direct construction so only the validating builder issues records.""" + raise TypeError( + "EgressDecisionEvidence objects must come from the evidence builder" + ) + def as_dict(self) -> dict[str, object]: """Return a detached JSON-compatible representation of this evidence.""" return { @@ -124,6 +141,30 @@ def as_dict(self) -> dict[str, object]: } +def _make_egress_decision_evidence( + *, + schema_version: str, + authority: str, + allowed_methods: tuple[str, ...], + address_count: int, + ipv4_address_count: int, + ipv6_address_count: int, + policy_fingerprint: str, + decision_fingerprint: str, +) -> EgressDecisionEvidence: + """Create one record only after the public builder completes revalidation.""" + evidence = object.__new__(EgressDecisionEvidence) + object.__setattr__(evidence, "schema_version", schema_version) + object.__setattr__(evidence, "authority", authority) + object.__setattr__(evidence, "allowed_methods", allowed_methods) + object.__setattr__(evidence, "address_count", address_count) + object.__setattr__(evidence, "ipv4_address_count", ipv4_address_count) + object.__setattr__(evidence, "ipv6_address_count", ipv6_address_count) + object.__setattr__(evidence, "policy_fingerprint", policy_fingerprint) + object.__setattr__(evidence, "decision_fingerprint", decision_fingerprint) + return evidence + + def build_egress_decision_evidence( validated: ValidatedEgressURL, *, @@ -151,7 +192,7 @@ def build_egress_decision_evidence( "policy_fingerprint": policy_digest, } decision_digest = _sha256_canonical_json(evidence_payload) - return EgressDecisionEvidence( + return _make_egress_decision_evidence( schema_version=DECISION_EVIDENCE_SCHEMA_VERSION, authority=str(evidence_payload["authority"]), allowed_methods=tuple(sorted(policy.allowed_methods)), From 4e2029b2eadef03d750a7c12bb0ed269de03405d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 17:01:14 +0900 Subject: [PATCH 3/5] test(evidence): require stable no-argument rejection --- tests/test_decision_evidence_factory_only.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/test_decision_evidence_factory_only.py b/tests/test_decision_evidence_factory_only.py index ab5f8c5..cde91f6 100644 --- a/tests/test_decision_evidence_factory_only.py +++ b/tests/test_decision_evidence_factory_only.py @@ -7,12 +7,14 @@ from egressweave import EgressDecisionEvidence +_DIRECT_CONSTRUCTION_ERROR = ( + "EgressDecisionEvidence objects must come from the evidence builder" +) + + def test_decision_evidence_rejects_direct_public_construction() -> None: """Require decision evidence to come from the validating builder.""" - with pytest.raises( - TypeError, - match="EgressDecisionEvidence objects must come from the evidence builder", - ): + with pytest.raises(TypeError, match=_DIRECT_CONSTRUCTION_ERROR): EgressDecisionEvidence( schema_version="egressweave.decision-evidence.v1", authority="api.example.com:443", @@ -23,3 +25,9 @@ def test_decision_evidence_rejects_direct_public_construction() -> None: policy_fingerprint="0" * 64, decision_fingerprint="1" * 64, ) + + +def test_decision_evidence_rejects_empty_direct_construction_consistently() -> None: + """Keep the intentional factory-only error for argument-shape mistakes.""" + with pytest.raises(TypeError, match=_DIRECT_CONSTRUCTION_ERROR): + EgressDecisionEvidence() From 4f39af67a6247b1ba84af3336d67755581ea9ffd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 17:01:51 +0900 Subject: [PATCH 4/5] fix(evidence): reject every direct construction shape --- src/egressweave/decision_evidence.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/egressweave/decision_evidence.py b/src/egressweave/decision_evidence.py index 068663f..198063c 100644 --- a/src/egressweave/decision_evidence.py +++ b/src/egressweave/decision_evidence.py @@ -111,18 +111,9 @@ class EgressDecisionEvidence: policy_fingerprint: str decision_fingerprint: str - def __init__( - self, - schema_version: str, - authority: str, - allowed_methods: tuple[str, ...], - address_count: int, - ipv4_address_count: int, - ipv6_address_count: int, - policy_fingerprint: str, - decision_fingerprint: str, - ) -> None: - """Reject direct construction so only the validating builder issues records.""" + def __init__(self, *args: object, **kwargs: object) -> None: + """Reject every direct construction shape in favor of the validating builder.""" + del args, kwargs raise TypeError( "EgressDecisionEvidence objects must come from the evidence builder" ) From 3ed76532a78012789fe5c65d617d4bf49cc17e4c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 17:07:32 +0900 Subject: [PATCH 5/5] style(tests): normalize factory-only import spacing --- tests/test_decision_evidence_factory_only.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_decision_evidence_factory_only.py b/tests/test_decision_evidence_factory_only.py index cde91f6..940ed38 100644 --- a/tests/test_decision_evidence_factory_only.py +++ b/tests/test_decision_evidence_factory_only.py @@ -6,7 +6,6 @@ from egressweave import EgressDecisionEvidence - _DIRECT_CONSTRUCTION_ERROR = ( "EgressDecisionEvidence objects must come from the evidence builder" )