From d9e167fea0e8fb4be0f5d58164e87e7cef0f955f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 05:19:40 +0900 Subject: [PATCH 1/6] test: expose timeout policy subclass dispatch --- 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 a7af737ab83e704f219a583ac8d50d11647fe109 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 05:23:52 +0900 Subject: [PATCH 2/6] security: require exact request-timeout policy type --- src/egressweave/policy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/egressweave/policy.py b/src/egressweave/policy.py index 1c69a1ff..8fbcaa26 100644 --- a/src/egressweave/policy.py +++ b/src/egressweave/policy.py @@ -171,7 +171,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" ) From bd38992d9a530e439afc0b2e7f7dbe4784426157 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 05:25:33 +0900 Subject: [PATCH 3/6] test: require exact timeout policy documentation --- .../test_timeout_policy_type_documentation.py | 39 +++++++++++++++++++ 1 file changed, 39 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..4da53b1b --- /dev/null +++ b/tests/test_timeout_policy_type_documentation.py @@ -0,0 +1,39 @@ +"""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" +CHANGELOG_PATH = REPOSITORY_ROOT / "CHANGELOG.md" + + +def _read(path: Path) -> str: + """Return one repository text file as UTF-8.""" + 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 a supported boundary.""" + guide = _read(TIMEOUT_GUIDE_PATH) + + for fragment in ( + "exact `EgressTimeoutPolicy` type", + "subclass", + "trusted policy construction", + "`as_httpcore_timeout()`", + ): + assert fragment in guide + + +def test_changelog_records_timeout_policy_type_hardening() -> None: + """Expose the pre-1.0 policy-integrity tightening to integrators.""" + changelog = _read(CHANGELOG_PATH) + + for fragment in ( + "request timeout policy", + "exact `EgressTimeoutPolicy`", + "subclass", + ): + assert fragment in changelog From e878c6ee9341c0d2b3fdb5ba4df3f99210e61466 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 06:01:38 +0900 Subject: [PATCH 4/6] docs: define exact timeout policy 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..1cb99b8b 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. +Trusted policy construction 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 +previously 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 executing inside the embedding +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 086ac16c5e0b304680564ea53f3f2797ed9fb7b1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 06:03:27 +0900 Subject: [PATCH 5/6] docs: record exact timeout policy hardening --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1e9dc0c..98600e19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). without changing the centrally managed review-agent credential contract. ### Security +- Require the request timeout policy to use the exact `EgressTimeoutPolicy` type + during trusted construction. Timeout-policy subclasses are rejected before + transport dispatch can dynamically invoke an overridden `as_httpcore_timeout()`, + preserving the reviewed finite ceilings as the authoritative configuration. - Canonicalize the public manifest writer's optional `forbidden_root` before any output-parent creation or output-path access. Missing, non-directory, symlinked, unresolvable, or otherwise noncanonical roots now fail with one From b803faac4ace5946f168c9e7403bfa529e6a1c2d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 06:06:42 +0900 Subject: [PATCH 6/6] docs: satisfy timeout policy construction contract --- docs/research/request-timeout-boundaries.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/research/request-timeout-boundaries.md b/docs/research/request-timeout-boundaries.md index 1cb99b8b..1d0996b7 100644 --- a/docs/research/request-timeout-boundaries.md +++ b/docs/research/request-timeout-boundaries.md @@ -14,15 +14,15 @@ HTTPX timeout extension immediately before HTTPCore dispatch: - malformed maps, unknown keys, booleans, negative values, and non-finite numbers fail through the generic `EgressNotAllowedError` boundary. -Trusted policy construction 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 -previously 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 executing inside the embedding -process. +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 previously 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 executing +inside the embedding 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