Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
485873e
test: reproduce timeout-policy subclass dispatch
seonghobae Aug 11, 2026
949c543
fix: seal request-timeout policy to exact type
seonghobae Aug 11, 2026
24dbd89
docs(security): preserve timeout-policy boundary guidance
seonghobae Aug 11, 2026
0109bd3
merge: refresh timeout boundary on protected main 72e7e79
seonghobae Aug 11, 2026
c6400d7
Merge origin/main into timeout policy reconstruction
seonghobae Aug 11, 2026
370d6d9
test: detect malformed timeout changelog structure
seonghobae Aug 11, 2026
1f0397c
docs: restore changelog security structure
seonghobae Aug 11, 2026
5a83a11
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae Aug 11, 2026
0794893
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae Aug 11, 2026
cda34a9
merge: update timeout policy on current main
seonghobae Aug 11, 2026
0694961
Merge branch 'main' of https://github.com/ContextualWisdomLab/EgressW…
seonghobae Aug 11, 2026
92437b8
test: keep timeout policy PR lint-clean
seonghobae Aug 11, 2026
b6f795a
Merge branch 'main' of https://github.com/ContextualWisdomLab/EgressW…
seonghobae Aug 11, 2026
a1fbc27
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae Aug 11, 2026
bbca2f7
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae Aug 11, 2026
f227105
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae Aug 11, 2026
0fb67e8
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae Aug 11, 2026
aacd5b3
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae Aug 11, 2026
f4d14ae
Merge protected main into timeout-policy reconstruction
seonghobae Aug 12, 2026
496d817
docs: restore timeout-policy release history
seonghobae Aug 12, 2026
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
disable the recurring loop.

### 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.
- Pin the credential-free verifier to a reviewed Python 3.13
`python@sha256:<64-hex>` digest, validate it before Docker execution, and
remove mutable-tag and `RepoDigests` promotion from the verifier boundary.
Expand Down
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 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
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
2 changes: 1 addition & 1 deletion src/egressweave/policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
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(),
)
49 changes: 49 additions & 0 deletions tests/test_timeout_policy_type_documentation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""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 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 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


def test_changelog_keeps_security_heading_and_entries_at_markdown_root() -> None:
"""Prevent whitespace drift from turning release-history structure into code."""
changelog = CHANGELOG_PATH.read_text(encoding="utf-8")

assert "\n### Security\n" in changelog
assert "\n- Require the request timeout policy" in changelog
assert "\n ### Security\n" not in changelog
assert "\n - Require the request timeout policy" not in changelog
Loading