Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/research/request-timeout-boundaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions src/egressweave/policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
return normalized in self.allowed_methods
33 changes: 33 additions & 0 deletions tests/test_timeout_policy_type_boundary.py
Original file line number Diff line number Diff line change
@@ -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(),
)
27 changes: 27 additions & 0 deletions tests/test_timeout_policy_type_documentation.py
Original file line number Diff line number Diff line change
@@ -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
Loading