Team Ai
Apppublic

openenv/echo_env

sourceHugging Faceupdated 1d agoView on Hugging Face
6likes
contracts.py277 linesDownload Raw Back to runtime
1"""Versioned runtime inputs and execution evidence, independent of a provider."""2 3import json4import math5import re6from dataclasses import dataclass7from pathlib import Path8from typing import Any, Literal9 10from pydantic import (11    BaseModel,12    ConfigDict,13    Field,14    field_validator,15    model_validator,16    ValidationError,17)18 19from ..manifest import ExecutionDeclaration, NetworkPolicy, ResourceDeclaration20 21MAX_PLAN_BYTES = 65_53622MAX_PLAN_ACTIONS = 10023MAX_JSON_DEPTH = 3224MAX_JSON_NODES = 10_00025 26 27class RuntimePlanError(ValueError):28    """A public runtime plan cannot be read safely or violates its data contract."""29 30 31def _check_json(value: Any, *, depth: int = 0, budget: list[int] | None = None) -> None:32    """Reject executable objects, non-finite numbers and excessive nesting."""33    if budget is None:34        budget = [MAX_JSON_NODES]35    budget[0] -= 136    if budget[0] < 0 or depth > MAX_JSON_DEPTH:37        raise ValueError("runtime JSON exceeds the node or nesting limit")38    if value is None or type(value) in (bool, int):39        return40    if type(value) is float:41        if not math.isfinite(value):42            raise ValueError("runtime JSON numbers must be finite")43        return44    if type(value) is str:45        return46    if type(value) is dict:47        if not all(type(key) is str for key in value):48            raise ValueError("runtime JSON object keys must be strings")49        children = value.values()50    elif type(value) is list:51        children = value52    else:53        raise ValueError("runtime inputs must contain only JSON data")54    for child in children:55        _check_json(child, depth=depth + 1, budget=budget)56 57 58class RuntimeReset(BaseModel):59    """60    Explicit reset inputs for one measured episode.61 62    Attributes:63        episode_id (`str`):64            Requested episode identity; verified by the state contract grader.65        seed (`int`):66            Requested seed. Acceptance alone does not establish determinism.67        options (`dict[str, Any]`, *optional*):68            Additional public reset arguments, never replacements for the seed or ID.69    """70 71    model_config = ConfigDict(extra="forbid", strict=True, frozen=True)72 73    episode_id: str = Field(min_length=1, max_length=128)74    seed: int = Field(ge=0, le=2**32 - 1)75    options: dict[str, Any] = Field(default_factory=dict)76 77    @field_validator("options")78    @classmethod79    def _public_options(cls, value: dict[str, Any]) -> dict[str, Any]:80        if {"episode_id", "seed"} & value.keys():81            raise ValueError("reset options cannot override episode_id or seed")82        _check_json(value)83        return value84 85 86class RuntimePlan(BaseModel):87    """88    Bounded, data-only runtime inputs. Capability declarations stay in the manifest.89 90    Attributes:91        plan_schema_version (`str`):92            The pinned sidecar schema version, `"1"`.93        reset ([`~openenv.validation.runtime.contracts.RuntimeReset`]):94            Inputs to the first reset on the measured WebSocket session.95        actions (`list[dict[str, Any]]`):96            Between one and 100 public actions, applied in order until termination.97    """98 99    model_config = ConfigDict(extra="forbid", strict=True, frozen=True)100 101    plan_schema_version: Literal["1"]102    reset: RuntimeReset103    actions: list[dict[str, Any]] = Field(min_length=1, max_length=MAX_PLAN_ACTIONS)104 105    @field_validator("actions")106    @classmethod107    def _data_actions(cls, value: list[dict[str, Any]]) -> list[dict[str, Any]]:108        _check_json(value)109        return value110 111    @model_validator(mode="after")112    def _total_size(self) -> "RuntimePlan":113        size = len(self.model_dump_json().encode("utf-8"))114        if size > MAX_PLAN_BYTES:115            raise ValueError(f"runtime plan exceeds {MAX_PLAN_BYTES} bytes")116        return self117 118 119def _unique_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:120    result = {}121    for key, value in pairs:122        if key in result:123            raise ValueError("duplicate runtime JSON key")124        result[key] = value125    return result126 127 128def load_runtime_plan(root: Path, execution: ExecutionDeclaration) -> RuntimePlan:129    """130    Read a bounded runtime plan without importing any subject code.131 132    Args:133        root (`Path`):134            Package root; the resolved plan must stay inside this directory.135        execution ([`~openenv.validation.manifest.ExecutionDeclaration`]):136            Manifest-owned location of the JSON sidecar.137 138    Returns:139        [`~openenv.validation.runtime.contracts.RuntimePlan`]: validated public inputs.140 141    Raises:142        [`~openenv.validation.runtime.contracts.RuntimePlanError`]:143            Missing, escaped, oversized, ambiguous or invalid input.144    """145    try:146        package_root = Path(root).resolve(strict=True)147        path = (package_root / execution.probe_path).resolve(strict=True)148        if not path.is_relative_to(package_root) or not path.is_file():149            raise ValueError("runtime plan must be a regular file inside the package")150        with path.open("rb") as source:151            payload = source.read(MAX_PLAN_BYTES + 1)152        if len(payload) > MAX_PLAN_BYTES:153            raise ValueError(f"runtime plan exceeds {MAX_PLAN_BYTES} bytes")154        raw = json.loads(payload, object_pairs_hook=_unique_object)155        _check_json(raw)156        return RuntimePlan.model_validate(raw)157    except ValidationError as exc:158        fields = RuntimePlan.model_fields.keys() | RuntimeReset.model_fields.keys()159        errors = []160        for error in exc.errors(161            include_input=False, include_context=False, include_url=False162        )[:5]:163            location = ".".join(164                str(part) if isinstance(part, int) or part in fields else "<field>"165                for part in error["loc"]166            )167            # These messages come from the fixed schema and validators; raw inputs,168            # exception context and subject-controlled field names are omitted.169            errors.append(f"{location or '<root>'}: {error['type']} ({error['msg']})")170        if exc.error_count() > 5:171            errors.append(f"... ({exc.error_count()} errors total)")172        raise RuntimePlanError(173            "runtime plan schema validation failed: " + "; ".join(errors)174        ) from exc175    except json.JSONDecodeError as exc:176        raise RuntimePlanError(177            f"invalid runtime plan JSON at line {exc.lineno}, column {exc.colno}: {exc.msg}"178        ) from exc179    except UnicodeError as exc:180        raise RuntimePlanError("runtime plan contains invalid text encoding") from exc181    except OSError as exc:182        raise RuntimePlanError(183            f"runtime plan could not be read ({type(exc).__name__})"184        ) from exc185    except (ValueError, RecursionError) as exc:186        raise RuntimePlanError(f"invalid runtime plan: {exc}") from exc187 188 189class LaunchSpec(BaseModel):190    """191    Explicit validation-only launch request; no caller environment is inherited.192 193    Attributes:194        image_ref (`str`):195            Immutable local image ID or repository digest.196        resources ([`~openenv.validation.manifest.ResourceDeclaration`]):197            Subject CPU, memory, writable-storage and episode budgets.198        network ([`~openenv.validation.manifest.NetworkPolicy`]):199            Requested policy; unsupported enforcement must be refused.200        run_id (`str`):201            Unique run-owned resource label, safe for provider names.202        startup_timeout_s (`float`, *optional*, defaults to 30):203            Independent readiness deadline, at most 300 seconds.204        env_vars (`dict[str, str]`, *optional*):205            Explicit environment only; providers do not forward host credentials.206    """207 208    model_config = ConfigDict(extra="forbid", frozen=True)209 210    image_ref: str = Field(pattern=r"^(?:[^@\s]+@)?sha256:[0-9a-f]{64}$")211    resources: ResourceDeclaration212    network: NetworkPolicy213    run_id: str = Field(pattern=r"^[a-z0-9][a-z0-9-]{0,62}$")214    startup_timeout_s: float = Field(default=30.0, gt=0.0, le=300.0)215    env_vars: dict[str, str] = Field(default_factory=dict, max_length=64)216 217    @field_validator("env_vars")218    @classmethod219    def _explicit_environment(cls, value: dict[str, str]) -> dict[str, str]:220        for name, content in value.items():221            if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name) is None:222                raise ValueError("invalid environment variable name")223            if "\x00" in content or len(content.encode("utf-8")) > 8192:224                raise ValueError("environment value contains NUL or exceeds 8192 bytes")225        return value226 227    @field_validator("resources")228    @classmethod229    def _finite_resources(cls, value: ResourceDeclaration) -> ResourceDeclaration:230        if not math.isfinite(value.cpu) or not math.isfinite(value.episode_timeout_s):231            raise ValueError("runtime resource limits must be finite")232        return value233 234 235@dataclass(frozen=True)236class WireExchange:237    """238    One immutable raw exchange, captured before client defaults or type coercion.239 240    Attributes:241        operation (`str`):242            The reset, step or state operation performed on the measured session.243        request_json (`str`):244            Original serialized request; parse only when grading.245        response_json (`str`):246            Original serialized response, including malformed JSON for diagnostics.247    """248 249    operation: Literal["reset", "step", "state"]250    request_json: str251    response_json: str252 253 254@dataclass(frozen=True)255class RuntimeEvidence:256    """257    Collector evidence shared by graders without permitting mutation of the episode.258 259    Attributes:260        exchanges (`tuple[WireExchange, ...]`):261            Ordered operations on one WebSocket session.262        observation_schema_json (`str`, *optional*):263            The advertised observation schema, exactly `/schema`'s observation value.264        failure_phase (`str`, *optional*):265            Collector phase that failed; a truncated transcript cannot pass silently.266        failure_reason (`str`, *optional*):267            Credential-safe explanation of the collection failure.268        reset_observation_schema_json (`str`, *optional*):269            Explicit `/schema` reset_observation value; absent means use the step schema.270    """271 272    exchanges: tuple[WireExchange, ...] = ()273    observation_schema_json: str | None = None274    failure_phase: str | None = None275    failure_reason: str | None = None276    reset_observation_schema_json: str | None = None277