Skip to content

Resource and security model

Talea is an in-process Python library. It validates and converts data inside the embedding application's process, with that process's CPU, memory, stack, file, network, and logging authority. Talea is not a sandbox for Python objects or application callbacks.

This document defines the technical threat model for external data boundaries. Vulnerability reporting and support governance are documented separately when the repository publishes those policies.

Overview

Python annotations resolve into one canonical schema. Talea compiles that schema into separate operations for strict validation, external Python input, JSON input, Python output, JSON output, and standards projection. External input is the security boundary: JSON is decoded and then passed to a schema-specialized converter; Python mappings are consumed directly by the same compiled input owner.

Component Responsibility Source owner
JSON decoder Strict default syntax and per-call codec boundary talea/input/json.py
Input compiler Mapping/decoded-tree conversion and validation talea/input/
Error model Bounded projections, rendering, and sensitive redaction talea/errors/
Recursive references Operation-local cycle detection talea/input/references.py, talea/input/artifacts.py
Declaration compiler Annotation resolution and generated operations talea/schema/, talea/spec/
Output compiler Python/JSON projection and output-cycle rejection talea/serialization/
Standards projector Finite JSON Schema and OpenAPI definition graphs talea/json_schema/

The important data flow is:

flowchart LR
    A[External JSON] --> B[Transport check]
    B --> C[Default or custom decoder]
    C --> D[Compiled input boundary]
    E[External Mapping] --> D
    D --> F[Validated value or Spec]
    D --> G[Validation or resource error]
    H[Trusted callbacks and codecs] --> C
    H --> D

The default JSON decoder rejects duplicate object keys and non-finite numeric constants and preserves fractional tokens as Decimal. A custom decoder owns its parser behavior. Talea still applies its compiled conversion and validation to the returned object.

Accepted resource architecture

ResourcePolicy is a frozen, slotted value owned by talea.resources. Its finite defaults are 8 MiB encoded JSON input, structural depth 64, 100,000 compiled schema visits, and 100 aggregated errors. Any dimension may be None when the caller owns that limit elsewhere. There is no mutable global setting.

Specs accept one per-call policy and otherwise use the immutable default. Contracts may retain one policy; an explicit per-call policy replaces it rather than merging layers. Each governed operation allocates one small mutable state object. Generated input operations share that state explicitly, including recursive back edges, so counters remain local to the call and independent across threads.

The governed operations are Spec.from_mapping(), Spec.from_json(), Contract.from_python(), and Contract.from_json(). Trusted Spec(...) construction, Contract.validate(), serialization, dynamic declaration, generic specialization, and standards projection are deliberately outside the policy. Their callers already control the Python objects or declaration graph; giving these operations superficially similar limit names would mix different trust and rollback semantics.

Depth is structural: a scalar root is zero, a root container is one, and each nested container adds one. A work node is one actual generated schema visit; scalars, containers, and attempted union alternatives count. Tagged unions visit only the selected branch. This total budget subsumes a per-container limit without conflicting thresholds. Strings, bytes, integer values, Decimal values, and individual containers have no separate resource dimension. Those are schema/application constraints; Python 3.14 also rejects excessively long integer-string conversions.

max_errors terminates independent Mapping/JSON failure aggregation at the first configured count. The ValidationError.truncated signal means traversal was terminated by the budget, not that Talea computed how many failures were omitted. Depth and node exhaustion raise ResourceLimitError with stable code, limit, and observed integers. The exception retains no input object.

Resource code Trigger Enforcement
input_size encoded JSON exceeds max_input_bytes reject before invoking the decoder
depth structural nesting exceeds max_depth reject at the first excessive path
nodes compiled visits exceed max_nodes reject with the first known excessive count

max_errors does not raise ResourceLimitError; it returns the deterministic error prefix in ValidationError with truncated=True.

Threat model, trust boundaries, and assumptions

The protected assets are process availability, bounded CPU and memory use, stack availability, deterministic validation, canonical schema integrity, sensitive rejected values, generated-code integrity, and the availability of schema tooling.

The relevant attackers are remote clients or message producers controlling JSON, same-process callers supplying Python values or Mapping implementations, application developers supplying callbacks and codecs, and tooling consumers supplying declarations. A remote JSON sender does not thereby control callbacks, annotations, codecs, process configuration, or release credentials.

The trust boundaries are:

  • JSON text, bytes, or byte arrays cross a transport and parser boundary.
  • Decoded containers and external Mapping objects cross a compiled traversal boundary.
  • Arbitrary Python objects passed to strict validation may cross an error representation boundary when rejected.
  • Annotation functions, dynamic declarations, transforms, checks, factories, serializers, patterns, and custom codecs are trusted application code.
  • Serialization and schema projection operate on application-owned values and declarations rather than remote transports by default.

Talea must preserve these invariants:

  • resource settings are immutable and operation-local;
  • no request can mutate process-global resource behavior;
  • the same limit has one canonical meaning across every governed input path;
  • deep or broad hostile input fails deliberately before accidental recursion or unbounded aggregation;
  • resource exceptions retain numeric observations and bounded metadata, never the offending payload;
  • sensitive validation failures continue to discard raw values and callback causes;
  • compiler-owned identifiers and bound runtime objects remain separate from user-provided names;
  • cycle state and resource counters cannot leak between calls or threads.

Attack surface, mitigations, and attacker stories

The scenarios below are threat hypotheses. They describe the capability a control must prevent; they are not vulnerability findings by themselves.

Priority Scenario and capability gain Prerequisites Impact Existing controls Implemented control or decision
High Oversized JSON is fully parsed before rejection Attacker controls a reachable JSON boundary with no earlier transport limit CPU and memory exhaustion Strict syntax Encoded size is checked before every decoder, including custom codecs
High Deep acyclic input exhausts the Python stack Recursive contract and attacker-controlled nested values Process or request failure through RecursionError Runtime cycles are rejected One depth budget spans recursive Spec, alias, and TypedDict input
High Broad input consumes unbounded traversal work Large or repeatedly nested containers CPU and allocation pressure Compiled O(n) traversal Generated input consumes a total actual-work node budget
High Invalid broad mappings create thousands of rich errors Many invalid or unexpected members CPU and retained-memory amplification Individual representations are bounded Canonical aggregation stops at max_errors and exposes truncation
Medium A large malformed document remains retained by its exception Caller retains a non-sensitive parser failure Memory retention beyond request handling Rendering and causes are bounded Resource failures retain no input; parser causes are omitted for large transports
Medium Pathological untagged unions repeat expensive work Developer declares many plausible branches and attacker forces late failure CPU and diagnostic amplification Canonical order; tagged unions dispatch directly Actual attempted branch work consumes the shared node budget
Medium Hostile custom Mapping methods or repr execute arbitrary work Same-process/plugin object injection CPU, side effects, or propagated application exceptions State cleanup uses finally; representation exceptions are caught No sandbox claim; failures cannot corrupt later operation state
Medium Developer-authored regex catastrophically backtracks Trusted pattern plus attacker-controlled string CPU exhaustion Pattern compiles once; source is not interpolated Trusted code; application review, constraints, deadline, or isolation owns mitigation
Low Dynamic declarations or specializations create compilation storms Application exposes trusted declaration APIs to high-cardinality input CPU and live-object pressure Weak specialization and introspection caches; per-Contract ownership Kept outside remote-input policy; caller owns declaration cardinality
Low Schema projection produces a very large document Tooling operates on a very broad trusted graph Tooling CPU and memory pressure Recursive definitions are finite and per-call results are not cached Kept outside remote-input policy; tooling caller owns graph breadth

For the accepted input controls, the attack path is always:

attacker-controlled transport or structure
    -> public from_json/from_mapping/from_python operation
    -> parser or generated traversal would consume resources
    -> pre-parse or compiled enforcement point
    -> bounded ResourceLimitError or truncated ValidationError

Custom decoders can be protected before invocation by the transport limit and after invocation by Talea's compiled traversal limits. Talea cannot bound work performed inside the decoder. The same rule applies to transforms, checks, factories, serializers, and pattern execution.

Severity calibration

Critical requires a source-backed path to code execution, credential theft, cross-boundary secret disclosure, or equivalent process authority not already held by the attacker. Ordinary resource exhaustion is not Critical.

High includes remotely reachable, inexpensive input that reliably exhausts process memory or the Python stack, or causes sustained disproportionate CPU without an effective application control. A library-only hypothesis is reduced when no reachable deployment is established.

Medium includes bounded-request denial of service, material error or memory amplification, or conditional secret retention that requires an application to retain or publish the exception. Developer-authored regex and union shapes are normally conditional Medium risks because the attacker does not control the declaration.

Low includes trusted tooling or declaration-time exhaustion, weak-cache creation pressure that requires the caller to keep objects alive, and stale documentation without a direct security effect. Authorized callback behavior and effects limited to an attacker-controlled local process are not privilege gains.

Caller-owned controls

Talea does not preempt Python code, recover globally from MemoryError, impose timeouts, or sandbox custom objects. Applications remain responsible for request concurrency, wall-clock deadlines, process isolation, operating-system limits, safe callback and regex review, and any stricter transport limits enforced before Talea receives a complete payload. This technical model does not replace a repository vulnerability-reporting policy or application threat model.

Executable hostile-input scenarios

The following program exercises a secret-bearing nested failure, oversized JSON rejected before decoding, excessive recursion depth, an exhausted compiled-node budget, truncated broad error aggregation, and a custom Mapping callback that demonstrates the sandbox boundary.

"""Hostile boundary examples: redaction, size, depth, work, and error budgets."""

from collections.abc import Callable, Mapping
from typing import Annotated

from talea import Contract, ResourceLimitError, ResourcePolicy, Sensitive, Spec, ValidationError


class Credentials(Spec):
    username: str
    token: Annotated[str, Sensitive()]


class SessionRequest(Spec):
    credentials: Credentials
    scopes: list[str]


secret = "never-log-this-token"
try:
    SessionRequest.from_mapping({"credentials": {"username": "ada", "token": 42}, "scopes": ["read"]})
except ValidationError as error:
    detail = error.errors()[0]
    assert detail["location"] == ["credentials", "token"]
    assert detail["input"] == "<redacted>"
    assert secret not in str(error)
else:
    raise AssertionError("invalid secret-bearing input must fail")


def expect_resource_error(operation: Callable[[], object], code: str) -> None:
    try:
        operation()
    except ResourceLimitError as error:
        assert error.code == code
        assert error.observed > error.limit
        assert not hasattr(error, "input")
    else:
        raise AssertionError(f"expected {code!r} resource rejection")


expect_resource_error(
    lambda: SessionRequest.from_json(
        '{"credentials":{"username":"ada","token":"' + secret + '"},"scopes":[]}',
        policy=ResourcePolicy(max_input_bytes=32),
    ),
    "input_size",
)

type Nested = int | list[Nested]
nested: Contract[Nested] = Contract(Nested)
expect_resource_error(
    lambda: nested.from_json("[[[[1]]]]", policy=ResourcePolicy(max_depth=3)),
    "depth",
)
expect_resource_error(
    lambda: Contract(list[int]).from_python(
        list(range(20)),
        policy=ResourcePolicy(max_nodes=8),
    ),
    "nodes",
)


class Batch(Spec):
    primary_id: int
    secondary_id: int
    revision: int
    attempts: int


try:
    Batch.from_mapping(
        {
            "primary_id": "bad",
            "secondary_id": "bad",
            "revision": "bad",
            "attempts": "bad",
        },
        policy=ResourcePolicy(max_errors=3),
    )
except ValidationError as error:
    assert error.truncated is True
    assert len(error.errors()) == 3
    assert [item["location"] for item in error.errors()] == [
        ["primary_id"],
        ["secondary_id"],
        ["revision"],
    ]
else:
    raise AssertionError("the error budget must terminate broad aggregation")


class HostileMapping(Mapping[str, object]):
    """Show that custom Mapping behavior is application code, not sandboxed input."""

    def __getitem__(self, key: str) -> object:
        raise RuntimeError(f"application mapping callback for {key}")

    def __iter__(self):
        return iter(("credentials",))

    def __len__(self) -> int:
        return 1


try:
    SessionRequest.from_mapping(HostileMapping())
except RuntimeError as error:
    assert "application mapping callback" in str(error)
else:
    raise AssertionError("Talea does not claim to sandbox hostile Python callbacks")

# Custom JSON decoders, transforms, checks, serializers, regular expressions,
# and Mapping methods execute with the embedding process's authority. Resource
# policy bounds Talea-owned transport and traversal work, not arbitrary Python.

These cases should be tested with the endpoint's actual maximum shape. A policy that admits a 500-item batch must count the nested object and scalar visits, not only the list length. Conversely, raising max_nodes globally because one batch is large weakens unrelated endpoints. Prefer immutable policies retained per Contract or selected per operation.

When handling errors, keep ResourceLimitError separate from ValidationError. The former reports code, limit, and observed and does not retain the payload. The latter reports contract invalidity and may contain an intentionally bounded prefix of independent failures; truncated=True means the error budget stopped traversal. Neither exception chooses an HTTP, queue, or retry policy for the application.

Sensitive redaction is deliberately conservative. A reachable Sensitive field can redact broader discriminator diagnostics so another failure cannot reveal secret-adjacent structure. Redaction protects Talea-owned errors and repr; it does not erase valid objects, prevent explicit serialization, protect application logs that access a field directly, or neutralize a malicious callback.