Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
disable the recurring loop.

### 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.
- Erase private request-method normalization exception provenance from
caller-visible policy denials.
- Pin the credential-free verifier to a reviewed Python 3.13
Expand Down
76 changes: 76 additions & 0 deletions docs/research/policy-configuration-integrity.md
Original file line number Diff line number Diff line change
@@ -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
8 changes: 4 additions & 4 deletions src/egressweave/_policy_normalization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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

Expand Down
125 changes: 125 additions & 0 deletions tests/test_policy_integer_value_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""Security contracts for exact built-in integer policy values."""

from __future__ import annotations

from pathlib import Path

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)],
)


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:
"""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_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:
"""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


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
Loading