From a460ca5927c19ae593d70e508e7829dd772252ec Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 17:17:20 +0900 Subject: [PATCH 1/4] test(policy): expose timeout subclass dispatch boundary --- tests/test_timeout_policy_type_boundary.py | 33 ++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/test_timeout_policy_type_boundary.py diff --git a/tests/test_timeout_policy_type_boundary.py b/tests/test_timeout_policy_type_boundary.py new file mode 100644 index 00000000..180b9b32 --- /dev/null +++ b/tests/test_timeout_policy_type_boundary.py @@ -0,0 +1,33 @@ +"""Regression contracts for the exact request-timeout policy type boundary.""" + +from __future__ import annotations + +import pytest + +from egressweave import EgressPolicy, EgressTimeoutPolicy + + +class _HostileTimeoutPolicy(EgressTimeoutPolicy): + """Model a subclass that can replace the reviewed timeout export method.""" + + def as_httpcore_timeout(self) -> dict[str, float]: + """Fail if a later transport dynamically dispatches this override.""" + raise AssertionError("subclass-controlled timeout export executed") + + +def test_host_policy_rejects_timeout_policy_subclass() -> None: + """Reject non-exact timeout policy types at trusted policy construction.""" + with pytest.raises(TypeError, match="request_timeout_policy"): + EgressPolicy.from_hosts( + "api.example.com", + request_timeout_policy=_HostileTimeoutPolicy(), + ) + + +def test_exact_authority_policy_rejects_timeout_policy_subclass() -> None: + """Apply the same exact-type boundary to the authority-pair constructor.""" + with pytest.raises(TypeError, match="request_timeout_policy"): + EgressPolicy.from_authorities( + [("api.example.com", 443)], + request_timeout_policy=_HostileTimeoutPolicy(), + ) From 0b72ac4bd946f7a4aa4d6668f701859e03e45cdb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 17:22:58 +0900 Subject: [PATCH 2/4] fix(policy): require exact timeout policy type --- src/egressweave/policy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/egressweave/policy.py b/src/egressweave/policy.py index 80afe952..f92dfbbc 100644 --- a/src/egressweave/policy.py +++ b/src/egressweave/policy.py @@ -178,7 +178,7 @@ def __post_init__(self) -> None: """Validate and canonicalize every immutable policy field.""" if not isinstance(self.allow_local, bool): raise TypeError("allow_local must be a boolean") - if not isinstance(self.request_timeout_policy, EgressTimeoutPolicy): + if type(self.request_timeout_policy) is not EgressTimeoutPolicy: raise TypeError( "request_timeout_policy must be an EgressTimeoutPolicy" ) @@ -511,4 +511,4 @@ def allows_http_method(self, method: str) -> bool: normalized = _normalize_allowed_method(method) except (TypeError, ValueError): return False - return normalized in self.allowed_methods \ No newline at end of file + return normalized in self.allowed_methods From 90dc3f4eaf9d994c02afcf4f4cbdf9f7fa661508 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 17:23:39 +0900 Subject: [PATCH 3/4] docs(policy): explain exact timeout type boundary --- docs/research/request-timeout-boundaries.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/research/request-timeout-boundaries.md b/docs/research/request-timeout-boundaries.md index 8b8ec4d8..38f26072 100644 --- a/docs/research/request-timeout-boundaries.md +++ b/docs/research/request-timeout-boundaries.md @@ -14,6 +14,16 @@ HTTPX timeout extension immediately before HTTPCore dispatch: - malformed maps, unknown keys, booleans, negative values, and non-finite numbers fail through the generic `EgressNotAllowedError` boundary. +The trusted policy-construction boundary accepts only the exact +`EgressTimeoutPolicy` type. Subclass polymorphism is not a supported extension +mechanism because transport binding later invokes `as_httpcore_timeout()`: a +subclass could otherwise replace that reviewed export path after startup +validation. Applications that supplied an `EgressTimeoutPolicy` subclass must +migrate to an exact instance configured through the documented immutable timeout +fields. This secure-default boundary keeps declarative values authoritative; it +does not claim to sandbox arbitrary trusted Python code executing in the host +process. + Policy maxima must be greater than zero. A request may still choose zero as an immediate, stricter timeout. The sanitized mapping is detached from caller-owned state and preserves unrelated safe extensions, including the validated TLS @@ -49,6 +59,8 @@ response data. ## Security properties +- **Exact trusted policy type:** construction rejects timeout-policy subclasses + before any later transport export can dynamically dispatch subclass code. - **No timeout disablement:** missing and `None` phase values become finite. - **No weaker override:** a request cannot exceed the immutable policy cap. - **Stricter caller control:** non-negative values below the cap are retained. From b0e9b43d4b3c8701e8b6af7a93f1e82a0e9030c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 17:24:04 +0900 Subject: [PATCH 4/4] test(docs): lock timeout type migration guidance --- .../test_timeout_policy_type_documentation.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/test_timeout_policy_type_documentation.py diff --git a/tests/test_timeout_policy_type_documentation.py b/tests/test_timeout_policy_type_documentation.py new file mode 100644 index 00000000..bbe60c67 --- /dev/null +++ b/tests/test_timeout_policy_type_documentation.py @@ -0,0 +1,27 @@ +"""Documentation contracts for exact request-timeout policy configuration.""" + +from __future__ import annotations + +from pathlib import Path + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +TIMEOUT_GUIDE_PATH = REPOSITORY_ROOT / "docs" / "research" / "request-timeout-boundaries.md" + + +def _read(path: Path) -> str: + """Return one repository text file as normalized UTF-8 prose.""" + return " ".join(path.read_text(encoding="utf-8").split()) + + +def test_timeout_guide_requires_exact_reviewed_policy_type() -> None: + """Explain why timeout-policy subclass polymorphism is not supported.""" + guide = _read(TIMEOUT_GUIDE_PATH) + + for fragment in ( + "exact `EgressTimeoutPolicy` type", + "subclass", + "trusted policy-construction boundary", + "`as_httpcore_timeout()`", + "must migrate", + ): + assert fragment in guide