From ea0ddf896aa10b1c539ddb80b4cc1a7e0161065a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 06:48:35 +0900 Subject: [PATCH 1/7] test: reconstruct exact scalar integer boundary on current main --- tests/test_policy_integer_value_types.py | 61 ++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/test_policy_integer_value_types.py diff --git a/tests/test_policy_integer_value_types.py b/tests/test_policy_integer_value_types.py new file mode 100644 index 00000000..9b0873b9 --- /dev/null +++ b/tests/test_policy_integer_value_types.py @@ -0,0 +1,61 @@ +"""Security contracts for exact built-in integer policy values.""" + +from __future__ import annotations + +import pytest + +from egressweave import EgressPolicy + + +class _PolicyIntegerSubclass(int): + """Represent an unreviewed integer subclass crossing trusted configuration.""" + + +def test_policy_rejects_integer_subclass_for_allowed_port() -> None: + """Reject non-exact ports before retaining normalized authority state.""" + with pytest.raises(TypeError, match="allowed_ports"): + EgressPolicy.from_hosts( + "api.example.com", + allowed_ports=[_PolicyIntegerSubclass(443)], + ) + + +@pytest.mark.parametrize( + "field_name", + [ + "max_resolved_addresses", + "max_request_header_fields", + "max_request_bytes", + ], +) +def test_policy_rejects_integer_subclass_for_resource_limits(field_name: str) -> None: + """Reject non-exact integers before retaining finite resource limits.""" + with pytest.raises(TypeError, match=field_name): + EgressPolicy.from_hosts( + "api.example.com", + **{field_name: _PolicyIntegerSubclass(8)}, # type: ignore[arg-type] + ) + + +def test_policy_keeps_exact_integer_and_decimal_string_configuration() -> None: + """Preserve reviewed exact integers and decimal environment values.""" + exact_integer = EgressPolicy.from_hosts( + "api.example.com", + allowed_ports=[8443], + max_resolved_addresses=8, + max_request_header_fields=32, + max_request_bytes=4096, + ) + decimal_string = EgressPolicy.from_hosts( + "api.example.com", + allowed_ports=["8443"], + max_resolved_addresses="8", + max_request_header_fields="32", + max_request_bytes="4096", + ) + + assert decimal_string == exact_integer + assert all(type(port) is int for port in decimal_string.allowed_ports) + assert type(decimal_string.max_resolved_addresses) is int + assert type(decimal_string.max_request_header_fields) is int + assert type(decimal_string.max_request_bytes) is int From b4c0ad32361ec2309caf913b330228a791c25c28 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 06:58:02 +0900 Subject: [PATCH 2/7] security: seal normalized integer policy values --- src/egressweave/_policy_normalization.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/egressweave/_policy_normalization.py b/src/egressweave/_policy_normalization.py index 8292fa31..0a190a5d 100644 --- a/src/egressweave/_policy_normalization.py +++ b/src/egressweave/_policy_normalization.py @@ -125,7 +125,7 @@ def _normalize_allowed_port(value: object) -> int | None: raise ValueError("allowed_ports entries must be decimal port numbers") port = int(normalized) else: - if isinstance(value, bool) or not isinstance(value, int): + if type(value) is not int: raise TypeError("allowed_ports entries must be integer port numbers") port = value @@ -188,7 +188,7 @@ def _normalize_max_resolved_addresses(value: object) -> int: ) address_count = int(normalized) else: - if isinstance(value, bool) or not isinstance(value, int): + if type(value) is not int: raise TypeError("max_resolved_addresses must be an integer count") address_count = value @@ -205,7 +205,7 @@ def _normalize_positive_count(value: object, field_name: str) -> int: raise ValueError(f"{field_name} must be a positive decimal count") item_count = int(normalized) else: - if isinstance(value, bool) or not isinstance(value, int): + if type(value) is not int: raise TypeError(f"{field_name} must be an integer count") item_count = value @@ -224,7 +224,7 @@ def _normalize_positive_byte_count(value: object, field_name: str) -> int: ) byte_count = int(normalized) else: - if isinstance(value, bool) or not isinstance(value, int): + if type(value) is not int: raise TypeError(f"{field_name} must be an integer byte count") byte_count = value From 2d520a7eacf8036014e0643544af076595f57c31 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 07:02:02 +0900 Subject: [PATCH 3/7] test: require policy value-integrity guidance --- tests/test_policy_integer_value_types.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_policy_integer_value_types.py b/tests/test_policy_integer_value_types.py index 9b0873b9..31cfbd21 100644 --- a/tests/test_policy_integer_value_types.py +++ b/tests/test_policy_integer_value_types.py @@ -2,6 +2,8 @@ from __future__ import annotations +from pathlib import Path + import pytest from egressweave import EgressPolicy @@ -59,3 +61,15 @@ def test_policy_keeps_exact_integer_and_decimal_string_configuration() -> None: assert type(decimal_string.max_resolved_addresses) is int assert type(decimal_string.max_request_header_fields) is int assert type(decimal_string.max_request_bytes) is int + + +def test_policy_configuration_integrity_guide_is_discoverable_and_current() -> None: + """Document the supported primitive-value boundary without sandbox claims.""" + guide_path = Path("docs/research/policy-configuration-integrity.md") + + assert guide_path.is_file() + guide = guide_path.read_text(encoding="utf-8") + assert "exact built-in `int`" in guide + assert "ASCII decimal strings" in guide + assert "does not make EgressWeave a Python sandbox" in guide + assert "https://docs.python.org/3.14/reference/datamodel.html" in guide From 965a1972bc6ad56be1ca6e2b515c10154d2e036c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 07:04:43 +0900 Subject: [PATCH 4/7] docs: define policy value-integrity boundary --- .../policy-configuration-integrity.md | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 docs/research/policy-configuration-integrity.md diff --git a/docs/research/policy-configuration-integrity.md b/docs/research/policy-configuration-integrity.md new file mode 100644 index 00000000..ea35b13f --- /dev/null +++ b/docs/research/policy-configuration-integrity.md @@ -0,0 +1,76 @@ +# Trusted policy configuration value integrity + +## Decision + +EgressWeave treats policy construction as a trusted startup boundary and stores a +canonical immutable policy value after normalization. Integer-form policy inputs +that become durable authority or resource-limit state therefore accept only an +exact built-in `int`. Non-exact integer subclasses are rejected instead of being +retained inside `EgressPolicy`. + +The reviewed environment-configuration contract remains unchanged: ASCII decimal +strings are accepted where the corresponding public field already supports them +and are converted to exact built-in integers before range checks, relational +checks, policy fingerprinting, DNS validation, request validation, or transport +delegation. Exact built-in integers continue to be accepted directly. + +This restriction applies to the shared integer normalization paths for allowed +ports, maximum resolved-address count, positive header-field counts, and positive +request/response byte budgets. It does not change configured defaults, allowed +ranges, authority pairing, DNS policy, TLS identity, proxy isolation, HTTP method +policy, request/response framing, or the generic request-time denial boundary. + +## Why exact type matters at this boundary + +Python deliberately supports subclassing immutable built-in types such as `int`, +and `isinstance(value, int)` is true for instances of subclasses. Python's data +model also permits subclasses of immutable built-ins to customize instance +creation. A broad `isinstance` check is therefore a polymorphism contract, not +proof that the stored object is the canonical built-in integer value expected by +a closed immutable policy representation. + +EgressWeave does not need that polymorphism for policy scalar fields. Supported +customization is expressed through documented values, not user-defined numeric +classes. Requiring `type(value) is int` on integer-form inputs prevents a subclass +object from surviving normalization and later participating in policy hashing or +equality, authority tuples, arithmetic or comparison boundaries, or provider +delegation. Environment text still reaches the same canonical state through +explicit decimal conversion. + +This supported-value sealing does not make EgressWeave a Python sandbox. Code +that is already executing inside the embedding process retains ordinary Python +capabilities. The boundary exists to make the documented policy value object +canonical, predictable, reviewable, and stable across standalone and modular +integrations. + +## Enforcement invariants + +1. Integer-form allowed ports must be exact built-in integers; reviewed ASCII + decimal strings are converted to built-in integers. +2. Integer-form DNS candidate limits must be exact built-in integers; reviewed + ASCII decimal strings are converted before positivity checks. +3. Shared positive field-count and byte-budget normalizers reject integer + subclasses and preserve their existing positive-value constraints. +4. Booleans remain invalid integer configuration even though Python defines + `bool` as an `int` subclass. +5. Existing decimal-string syntax, defaults, public builder signatures, and + request-time generic denial behavior remain unchanged. +6. Invalid trusted startup configuration continues to raise actionable + field-specific `TypeError` or `ValueError` rather than becoming an opaque + request-time policy denial. +7. Regression tests exercise the public `EgressPolicy` constructors so the + contract is proven at the API boundary rather than only against internal + helpers. + +## Operator migration + +Applications that supply plain integers or ASCII decimal environment values need +no change. Applications that pass custom subclasses of `int` for ports or finite +resource budgets should materialize an exact built-in integer before policy +construction. This is a pre-1.0 tightening of an ambiguous configuration shape; +it does not widen egress authority or change any finite default. + +## Reference — APA 7th + +Python Software Foundation. (2026). *Data model — Python 3.14.6 documentation*. +https://docs.python.org/3.14/reference/datamodel.html From d89444773e0b710d2cad1ed1aa21ca5cc4350257 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 07:07:10 +0900 Subject: [PATCH 5/7] test: require policy value-integrity changelog parity --- tests/test_policy_integer_value_types.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_policy_integer_value_types.py b/tests/test_policy_integer_value_types.py index 31cfbd21..d8967ef3 100644 --- a/tests/test_policy_integer_value_types.py +++ b/tests/test_policy_integer_value_types.py @@ -73,3 +73,10 @@ def test_policy_configuration_integrity_guide_is_discoverable_and_current() -> N assert "ASCII decimal strings" in guide assert "does not make EgressWeave a Python sandbox" in guide assert "https://docs.python.org/3.14/reference/datamodel.html" in guide + + +def test_changelog_records_shared_policy_integer_value_sealing() -> None: + """Record the trusted scalar policy tightening in release history.""" + changelog = Path("CHANGELOG.md").read_text(encoding="utf-8") + + assert "Reject non-exact integer subclasses in shared policy integer fields" in changelog From 6d8cde22200074060abd9f6e45ef8be52056dc39 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 07:11:11 +0900 Subject: [PATCH 6/7] docs: record scalar policy value sealing --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1e9dc0c..d2eef161 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 +- Reject non-exact integer subclasses in shared policy integer fields before + retaining trusted configuration state. Exact built-in integers and existing + ASCII decimal strings remain supported, preserving defaults and ranges while + preventing subclass-controlled values from crossing immutable policy construction. - 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 cbba06bcbe11348ba29380b122b6df78f728030c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 07:31:25 +0900 Subject: [PATCH 7/7] test: cover every shared integer policy path --- tests/test_policy_integer_value_types.py | 43 ++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/test_policy_integer_value_types.py b/tests/test_policy_integer_value_types.py index d8967ef3..f1a51126 100644 --- a/tests/test_policy_integer_value_types.py +++ b/tests/test_policy_integer_value_types.py @@ -22,12 +22,25 @@ def test_policy_rejects_integer_subclass_for_allowed_port() -> None: ) +def test_policy_rejects_integer_subclass_for_exact_authority_port() -> None: + """Reject a non-exact port in the exact-authority constructor as well.""" + with pytest.raises(TypeError, match="allowed_ports"): + EgressPolicy.from_authorities( + [("api.example.com", _PolicyIntegerSubclass(443))], + ) + + @pytest.mark.parametrize( "field_name", [ "max_resolved_addresses", "max_request_header_fields", + "max_response_header_fields", "max_request_bytes", + "max_response_bytes", + "max_response_header_bytes", + "max_request_header_bytes", + "max_request_target_bytes", ], ) def test_policy_rejects_integer_subclass_for_resource_limits(field_name: str) -> None: @@ -46,21 +59,51 @@ def test_policy_keeps_exact_integer_and_decimal_string_configuration() -> None: allowed_ports=[8443], max_resolved_addresses=8, max_request_header_fields=32, + max_response_header_fields=32, max_request_bytes=4096, + max_response_bytes=4096, + max_response_header_bytes=4096, + max_request_header_bytes=4096, + max_request_target_bytes=8192, ) decimal_string = EgressPolicy.from_hosts( "api.example.com", allowed_ports=["8443"], max_resolved_addresses="8", max_request_header_fields="32", + max_response_header_fields="32", max_request_bytes="4096", + max_response_bytes="4096", + max_response_header_bytes="4096", + max_request_header_bytes="4096", + max_request_target_bytes="8192", ) assert decimal_string == exact_integer assert all(type(port) is int for port in decimal_string.allowed_ports) assert type(decimal_string.max_resolved_addresses) is int assert type(decimal_string.max_request_header_fields) is int + assert type(decimal_string.max_response_header_fields) is int assert type(decimal_string.max_request_bytes) is int + assert type(decimal_string.max_response_bytes) is int + assert type(decimal_string.max_response_header_bytes) is int + assert type(decimal_string.max_request_header_bytes) is int + assert type(decimal_string.max_request_target_bytes) is int + + +def test_exact_authority_keeps_integer_and_decimal_string_port_equivalent() -> None: + """Preserve exact-authority ergonomics while storing canonical integer ports.""" + exact_integer = EgressPolicy.from_authorities([("api.example.com", 8443)]) + decimal_string = EgressPolicy.from_authorities([("api.example.com", "8443")]) + + assert decimal_string == exact_integer + assert decimal_string.allowed_authorities == frozenset( + {("api.example.com", 8443)} + ) + assert all(type(port) is int for port in decimal_string.allowed_ports) + assert all( + type(port) is int for _, port in decimal_string.allowed_authorities + ) def test_policy_configuration_integrity_guide_is_discoverable_and_current() -> None: