diff --git a/docs/research/connection-pool-resource-limits.md b/docs/research/connection-pool-resource-limits.md index 88471aa..d37788e 100644 --- a/docs/research/connection-pool-resource-limits.md +++ b/docs/research/connection-pool-resource-limits.md @@ -18,13 +18,18 @@ an exact `EgressConnectionPoolPolicy` with different documented field values instead. This boundary does not claim EgressWeave sandboxes arbitrary Python code already executing inside the embedding process. -`max_connections` must be a positive integer or ASCII decimal string. -`max_keepalive_connections` may be zero to retain no idle connections but must -not exceed total capacity. `keepalive_expiry_seconds` must be a finite -non-negative real number and may be zero for immediate expiry. Booleans, -fractional counts, signed or non-ASCII count text, negative values, non-finite -expiry values, unrelated objects, and contradictory capacities fail during -trusted policy construction. +`max_connections` must be an exact built-in `int` greater than zero or an exact +built-in `str` containing only ASCII decimal digits. +`max_keepalive_connections` accepts the same reviewed forms, may be zero to +retain no idle connections, and must not exceed total capacity. Integer and +string subclasses are rejected before normalization so caller-defined numeric +or text-protocol behavior cannot cross the trusted configuration boundary. A +caller using either subclass form must migrate by converting its validated value +to an exact built-in integer or exact ASCII decimal string before constructing +the policy. `keepalive_expiry_seconds` must be a finite non-negative real number +and may be zero for immediate expiry. Booleans, fractional counts, signed or +non-ASCII count text, negative values, non-finite expiry values, unrelated +objects, and contradictory capacities fail during trusted policy construction. ## Standards basis @@ -54,18 +59,22 @@ and portable across standalone and modular integrations. 1. Both public `EgressPolicy` constructors accept the same immutable pool policy. 2. Trusted construction accepts only the exact `EgressConnectionPoolPolicy` type; subclasses are rejected before transport pool values are read. -3. Total connection capacity is always positive and finite. -4. Idle capacity is finite, may be zero, and cannot exceed total capacity. -5. Idle expiry is finite and non-negative; `None` cannot disable reclamation. -6. Synchronous and asynchronous HTTPCore pools receive the exact normalized +3. Count fields accept only exact built-in integers or exact built-in ASCII + decimal strings; integer subclasses, string subclasses, and booleans are + rejected before numeric or text protocol methods are invoked. +4. Total connection capacity is always positive and finite. +5. Idle capacity is finite, may be zero, and cannot exceed total capacity. +6. Idle expiry is finite and non-negative; `None` cannot disable reclamation. +7. Synchronous and asynchronous HTTPCore pools receive the exact normalized values from the policy. -7. No transport imports HTTPX's private `DEFAULT_LIMITS` object. -8. The normalized pool policy participates in deterministic policy and decision +8. No transport imports HTTPX's private `DEFAULT_LIMITS` object. +9. The normalized pool policy participates in deterministic policy and decision fingerprints without recording live connection state. -9. Defaults, valid environment-style count text, invalid configuration, - relational invariants, exact policy-type enforcement, sync/async delegation, - public API exposure, and fingerprint drift are covered by offline regression - tests with complete production statement and branch coverage. +10. Defaults, valid environment-style count text, invalid configuration, + relational invariants, exact policy-type and scalar-type enforcement, + sync/async delegation, public API exposure, and fingerprint drift are covered + by offline regression tests with complete production statement and branch + coverage. ## Operational guidance @@ -80,8 +89,10 @@ assuming it is universally safer. Applications that previously subclassed `EgressConnectionPoolPolicy` must migrate to an exact instance and configure the supported finite fields directly. -The exact-type check runs during trusted startup, before a pool or request can -consume those values. +Applications that supplied integer or string subclasses for either count must +convert the validated count to an exact built-in `int` or exact built-in ASCII +decimal `str`. These exact-type checks run during trusted startup, before a pool +or request can consume those values. ## References diff --git a/src/egressweave/connection_pool_policy.py b/src/egressweave/connection_pool_policy.py index f372dc7..90e6d03 100644 --- a/src/egressweave/connection_pool_policy.py +++ b/src/egressweave/connection_pool_policy.py @@ -22,9 +22,9 @@ def _normalize_connection_count( """Return one exact ASCII-compatible connection-count limit.""" if isinstance(value, bool): raise TypeError(f"{field_name} must be an integer or ASCII decimal string") - if isinstance(value, int): + if type(value) is int: normalized = value - elif isinstance(value, str): + elif type(value) is str: if not value or not value.isascii() or not value.isdecimal(): raise ValueError(f"{field_name} must be an ASCII decimal string") normalized = int(value, 10) @@ -56,10 +56,10 @@ class EgressConnectionPoolPolicy: ``max_connections`` is the maximum number of concurrent TCP connections the pool may own. ``max_keepalive_connections`` limits the subset retained while idle and may be zero to disable idle retention. Both count fields accept - exact integers or ASCII decimal strings for environment-derived settings. - ``keepalive_expiry_seconds`` limits how long an idle connection remains - reusable and may be zero for immediate expiry. The defaults preserve HTTPX's - documented finite baseline without importing HTTPX's private + exact integers or exact ASCII decimal strings for environment-derived + settings. ``keepalive_expiry_seconds`` limits how long an idle connection + remains reusable and may be zero for immediate expiry. The defaults preserve + HTTPX's documented finite baseline without importing HTTPX's private ``DEFAULT_LIMITS`` object. """ diff --git a/tests/test_connection_pool_count_value_documentation.py b/tests/test_connection_pool_count_value_documentation.py new file mode 100644 index 0000000..56f85d3 --- /dev/null +++ b/tests/test_connection_pool_count_value_documentation.py @@ -0,0 +1,24 @@ +"""Documentation contracts for exact connection-pool count value types.""" + +from __future__ import annotations + +from pathlib import Path + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +POOL_GUIDE_PATH = ( + REPOSITORY_ROOT / "docs" / "research" / "connection-pool-resource-limits.md" +) + + +def test_connection_pool_guide_documents_exact_builtin_count_values() -> None: + """Keep operator guidance aligned with the primitive-value integrity boundary.""" + guide = " ".join(POOL_GUIDE_PATH.read_text(encoding="utf-8").split()) + + for fragment in ( + "exact built-in `int`", + "exact built-in `str`", + "Integer and string subclasses are rejected", + "must migrate", + "exact built-in ASCII decimal `str`", + ): + assert fragment in guide diff --git a/tests/test_connection_pool_count_value_types.py b/tests/test_connection_pool_count_value_types.py new file mode 100644 index 0000000..f38dbfe --- /dev/null +++ b/tests/test_connection_pool_count_value_types.py @@ -0,0 +1,63 @@ +"""Security contracts for exact connection-pool count value types.""" + +from __future__ import annotations + +import pytest + +from egressweave import EgressConnectionPoolPolicy + + +class _ConnectionCountSubclass(int): + """Represent an unreviewed integer subclass crossing trusted configuration.""" + + +class _ExplodingCountString(str): + """Expose polymorphic string inspection in trusted count normalization.""" + + def isascii(self) -> bool: + """Fail if normalization invokes a subclass-controlled text method.""" + raise AssertionError("string subclass isascii executed") + + +@pytest.mark.parametrize( + "field_name", + ["max_connections", "max_keepalive_connections"], +) +def test_connection_pool_policy_rejects_integer_subclasses(field_name: str) -> None: + """Reject non-exact integers before retaining finite pool-capacity values.""" + with pytest.raises(TypeError, match=field_name): + EgressConnectionPoolPolicy( + **{field_name: _ConnectionCountSubclass(1)} # type: ignore[arg-type] + ) + + +@pytest.mark.parametrize( + "field_name", + ["max_connections", "max_keepalive_connections"], +) +def test_connection_pool_policy_rejects_string_subclasses_before_inspection( + field_name: str, +) -> None: + """Reject non-exact strings before invoking their text protocol methods.""" + with pytest.raises(TypeError, match=field_name): + EgressConnectionPoolPolicy( + **{field_name: _ExplodingCountString("1")} # type: ignore[arg-type] + ) + + +def test_connection_pool_policy_keeps_reviewed_count_input_forms() -> None: + """Continue accepting exact integers and reviewed ASCII decimal strings.""" + exact_integer = EgressConnectionPoolPolicy( + max_connections=8, + max_keepalive_connections=2, + ) + decimal_string = EgressConnectionPoolPolicy( + max_connections="8", + max_keepalive_connections="2", + ) + + assert type(exact_integer.max_connections) is int + assert type(exact_integer.max_keepalive_connections) is int + assert decimal_string == exact_integer + assert type(decimal_string.max_connections) is int + assert type(decimal_string.max_keepalive_connections) is int