Team Ai
Apppublic

openenv/echo_env

sourceHugging Faceupdated 1d agoView on Hugging Face
6likes
runner.py469 linesDownload Raw Back to validation
1"""Validation orchestration: parse โ†’ grade โ†’ apply policy โ†’ report."""2 3import hashlib4import os5import stat6import time7import uuid8from dataclasses import replace9from pathlib import Path10 11from .graders import GraderRegistry, Subject12from .graders.runtime import (13    ObservationSchemaGrader,14    RewardWellFormedGrader,15    StateContractGrader,16)17from .graders.static import StaticManifestGrader18from .manifest import ManifestError, NormalizedManifest, NormalizedManifestV219from .parsers import ParserRegistry20from .parsers.openenv_yaml import OpenEnvYamlParser21from .policy import apply_policy, load_policy, PolicyError, SeverityPolicy22from .providers import ProviderError, StartupError, UnsupportedCapability23from .report import CheckResult, ValidationReport, ValidationReportV224from .runtime.artifacts import write_runtime_bundle25from .runtime.collector import collect_runtime_evidence, RuntimeCollectionInterrupted26from .runtime.contracts import LaunchSpec, load_runtime_plan, RuntimePlanError27from .runtime.scheduler import execute_graders, order_graders28from .signature import detect_signature29from .types import CheckStatus, Lane, Level, ProviderCapability30 31REPORT_SCHEMA_VERSION = "1"32 33_DIGEST_EXCLUDED_DIRS = {".git", "__pycache__", ".venv", ".worktrees", "outputs"}34 35 36def source_digest(package_root: Path) -> str:37    """38    Deterministic sha256 over the package tree (relative paths + file contents).39 40    Args:41        package_root (`Path`):42            The package directory.43 44    Returns:45        `str`: a 64-character hex digest.46    """47    digest = hashlib.sha256()48    files = []49    for path in package_root.rglob("*"):50        relative_path = path.relative_to(package_root)51        if any(part in _DIGEST_EXCLUDED_DIRS for part in relative_path.parts):52            continue53        info = path.lstat()54        if stat.S_ISLNK(info.st_mode):55            raise ValueError("validation source may not contain symbolic links")56        if stat.S_ISREG(info.st_mode):57            files.append((relative_path.as_posix(), path, info))58        elif not stat.S_ISDIR(info.st_mode):59            raise ValueError("validation source must contain regular files")60 61    for relative_path, path, info in sorted(files, key=lambda item: item[0]):62        digest.update(relative_path.encode())63        digest.update(b"\0")64        flags = (65            os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)66        )67        fd = os.open(path, flags)68        with os.fdopen(fd, "rb") as source:69            opened = os.fstat(source.fileno())70            if not stat.S_ISREG(opened.st_mode):71                raise ValueError("validation source must contain regular files")72            if not os.path.samestat(info, opened):73                raise ValueError("validation source changed before it could be read")74            while chunk := source.read(1024 * 1024):75                digest.update(chunk)76        digest.update(b"\0")77    return digest.hexdigest()78 79 80def default_parser_registry() -> ParserRegistry:81    """The parsers shipped in this build."""82    registry = ParserRegistry()83    registry.register(OpenEnvYamlParser())84    return registry85 86 87def default_grader_registry(policy: SeverityPolicy) -> GraderRegistry:88    """The graders shipped in this build, including their selection metadata."""89    registry = GraderRegistry()90    registry.register(StaticManifestGrader(policy.bounds))91    registry.register(RewardWellFormedGrader())92    registry.register(ObservationSchemaGrader())93    registry.register(StateContractGrader())94    return registry95 96 97def _outcome(check_id, status, reason, *, started=None, measured=None):98    return CheckResult(99        check_id=check_id,100        status=status,101        evidence=[reason],102        measured=measured or {},103        duration_s=time.monotonic() - started if started is not None else 0,104    )105 106 107def _applicable(check_id, manifest):108    if manifest is None:109        return True110    if check_id in {"runtime.rubric_introspectable", "runtime.reward_attribution"}:111        return manifest.capabilities.rubric_tree112    if check_id == "runtime.task_declaration_accuracy":113        return manifest.capabilities.task_api or bool(114            manifest.capabilities.declared_task_count115        )116    return True117 118 119def _runtime(subject, graders, *, skip_build, provider):120    """Own build/start/collection/stop and retain failure evidence through teardown."""121    manifest = subject.manifest122    plan = None123    evidence = None124    running = None125    inspection = {}126    cleanup = {"required": False, "completed": True}127    started = time.monotonic()128    attempted = False129    result = None130    checks = []131    try:132        if skip_build:133            raise UnsupportedCapability(134                "--skip-build: build-dependent runtime checks skipped"135            )136        if not isinstance(manifest, NormalizedManifestV2):137            raise UnsupportedCapability(138                "missing validation.execution declaration and runtime plan"139            )140        plan = load_runtime_plan(subject.root, manifest.execution)141        if manifest.execution.requires_credentials:142            raise UnsupportedCapability(143                "credential_delivery is deferred for this release"144            )145        if provider is None:146            from .providers.docker import DockerValidationProvider147 148            provider = DockerValidationProvider()149        if manifest.network.mode not in provider.supported_network_modes:150            raise UnsupportedCapability(151                f"provider cannot enforce network mode {manifest.network.mode}"152            )153        if (154            manifest.resources.gpus155            and ProviderCapability.GPU not in provider.capabilities156        ):157            raise UnsupportedCapability("missing provider capability: gpu")158        if ProviderCapability.IMAGE_BUILD not in provider.capabilities:159            raise UnsupportedCapability("missing provider capability: image_build")160        # Validate all launch constraints before any container or build is started.161        spec = LaunchSpec(162            image_ref="sha256:" + "0" * 64,163            resources=manifest.resources,164            network=manifest.network,165            run_id="validation-" + uuid.uuid4().hex,166        )167        attempted = True168        image_ref = provider.build(subject.root, manifest.execution)169        running = provider.start(170            LaunchSpec.model_validate({**spec.model_dump(), "image_ref": image_ref})171        )172        cleanup = {"required": True, "completed": False}173        inspection = running.inspect()174        result = _outcome(175            "runtime.startup",176            CheckStatus.PASS,177            "subject built and reached its control endpoint",178            started=started,179            measured={"provider": provider.name, "image_ref": image_ref},180        )181        evidence = collect_runtime_evidence(182            running.base_url,183            plan,184            episode_timeout_s=manifest.resources.episode_timeout_s,185        )186        # Protocol collection must finish before its dependent contract checks run.187        if evidence.failure_reason:188            result = _outcome(189                "runtime.startup",190                CheckStatus.FAIL,191                evidence.failure_reason,192                started=started,193            )194        subject = replace(195            subject, image_ref=image_ref, running=running, runtime_evidence=evidence196        )197        checks = execute_graders(198            graders,199            subject,200            provider_capabilities=provider.capabilities,201            prior=[result],202        )203    except UnsupportedCapability as exc:204        result = _outcome(205            "runtime.startup", CheckStatus.SKIP, str(exc), started=started206        )207    except (RuntimePlanError, StartupError) as exc:208        result = _outcome(209            "runtime.startup",210            CheckStatus.FAIL,211            str(exc)[:4096],212            started=started,213        )214    except ProviderError as exc:215        result = _outcome(216            "runtime.startup",217            CheckStatus.ERROR,218            str(exc)[:4096],219            started=started,220        )221    except KeyboardInterrupt as exc:222        if isinstance(exc, RuntimeCollectionInterrupted):223            evidence = exc.evidence224        result = _outcome(225            "runtime.startup",226            CheckStatus.ERROR,227            "validation interrupted",228            started=started,229        )230    except Exception as exc:231        result = _outcome(232            "runtime.startup",233            CheckStatus.ERROR,234            f"runtime orchestration failed ({type(exc).__name__})",235            started=started,236        )237    finally:238        if running is not None:239            try:240                running.stop()241                cleanup["completed"] = True242            except (Exception, KeyboardInterrupt) as exc:243                cleanup["completed"] = False244                cleanup["reason"] = f"subject teardown failed ({type(exc).__name__})"245                if result is None:246                    result = _outcome(247                        "runtime.startup",248                        CheckStatus.ERROR,249                        "subject teardown failed",250                        started=started,251                    )252                else:253                    result.status = CheckStatus.ERROR254                    result.evidence.append("subject teardown failed")255                    result.duration_s = time.monotonic() - started256    return [result, *checks], attempted, plan, evidence, inspection, cleanup257 258 259def run_validation(260    target: Path,261    *,262    max_level: Level = Level.SEMANTIC,263    skip_build: bool = False,264    policy: SeverityPolicy | None = None,265    provider=None,266    artifacts_dir: Path | None = None,267) -> ValidationReport | ValidationReportV2:268    """269    Validate a package end to end and return the report.270 271    Raises [`~openenv.validation.signature.SignatureError`] for ambiguous or272    unrecognized packages and273    [`~openenv.validation.signature.UnsupportedPackageError`] for274    recognized-but-unsupported ones (CLI exit code 2). A package whose declarations275    fail the manifest schema yields a normal report with a `static.manifest` FAIL276    (exit code 1).277 278    Args:279        target (`Path`):280            The package directory.281        max_level ([`~openenv.validation.types.Level`], *optional*, defaults to `Level.SEMANTIC`):282            Level ceiling; graders above it are not selected.283        skip_build (`bool`, *optional*, defaults to `False`):284            Skip the image build; build-dependent checks SKIP with a reason.285        policy ([`~openenv.validation.policy.SeverityPolicy`], *optional*):286            `None` chooses v1 for static and v2 for runtime/semantic ceilings.287        provider ([`~openenv.validation.providers.ValidationProvider`], *optional*):288            Validation-only provider; defaults to Docker-local for runtime runs.289        artifacts_dir (`Path`, *optional*):290            Write a redacted reproduction bundle to this directory.291 292    Returns:293        [`~openenv.validation.report.ValidationReport`]: the completed report.294    """295    target = Path(target)296    wants_runtime = max_level >= Level.RUNTIME297    policy = policy or load_policy("v2" if wants_runtime else "v1")298    if wants_runtime and "runtime.startup" not in policy.entries_for_lane(Lane.LOCAL):299        raise PolicyError(300            "runtime validation requires policy v2; v1 supports --level static"301        )302    signature = detect_signature(target)303    try:304        digest_before = source_digest(target)305    except (ValueError, OSError):306        digest_before = ""307 308    parser = default_parser_registry().parser_for(signature)309    graders = default_grader_registry(policy)310    manifest: NormalizedManifest | None = None311    results: list[CheckResult] = []312    selected = {}313 314    parse_started = time.monotonic()315    if not digest_before:316        results.append(317            _outcome(318                "static.manifest",319                CheckStatus.ERROR,320                "package source could not be verified before validation",321                started=parse_started,322            )323        )324    else:325        try:326            manifest = parser.parse(target)327        except ManifestError as exc:328            results.append(329                CheckResult(330                    check_id="static.manifest",331                    status=CheckStatus.FAIL,332                    measured={"schema_errors": len(exc.errors)},333                    evidence=exc.errors,334                    remediation=exc.remediation,335                    duration_s=time.monotonic() - parse_started,336                )337            )338 339    levels = [Level.STATIC]340    plan = evidence = None341    inspection = {}342    cleanup = {"required": False, "completed": True}343    if manifest is not None:344        selected = {345            grader.check_id: grader for grader in graders.select(manifest, max_level)346        }347        subject = Subject(348            root=target,349            manifest=manifest,350            image_ref=None,351            running=None,352            outputs_dir=target / "outputs",353        )354        results.extend(355            execute_graders(356                [357                    grader358                    for grader in selected.values()359                    if grader.level is Level.STATIC360                ],361                subject,362            )363        )364        if wants_runtime:365            runtime_results, attempted, plan, evidence, inspection, cleanup = _runtime(366                subject,367                [368                    grader369                    for grader in selected.values()370                    if grader.level is Level.RUNTIME371                ],372                skip_build=skip_build,373                provider=provider,374            )375            results.extend(runtime_results)376            if attempted:377                levels.append(Level.RUNTIME)378 379    if wants_runtime:380        # Policy IDs are an inventory, not evidence that a grader exists. Keep the381        # incomplete surface explicit throughout the staged implementation.382        present = {result.check_id for result in results}383        for entry in policy.entries_for_lane(Lane.LOCAL).values():384            grader = graders.get(entry.check_id)385            applicable = (386                manifest is None387                or entry.check_id in selected388                or (grader is None and _applicable(entry.check_id, manifest))389            )390            if (391                entry.level in {Level.RUNTIME, Level.SEMANTIC}392                and entry.level <= max_level393                and entry.check_id not in present394                and applicable395            ):396                reason = "grader not implemented in this build"397                if manifest is None:398                    reason = "unmet dependency: valid manifest"399                elif grader is not None and grader.depends_on:400                    reason = "unmet dependency: " + ", ".join(grader.depends_on)401                results.append(_outcome(entry.check_id, CheckStatus.SKIP, reason))402        source_problem = None403        if not digest_before:404            source_problem = "package source could not be verified before validation"405        else:406            try:407                digest_after = source_digest(target)408            except (ValueError, OSError):409                digest_after = None410            if digest_after != digest_before:411                source_problem = (412                    "package source changed during validation"413                    if digest_after is not None414                    else "package source could not be verified after validation"415                )416        if source_problem is not None:417            invalidated = {"runtime.startup"}418            for grader in order_graders(list(selected.values())):419                if invalidated.intersection(grader.depends_on):420                    invalidated.add(grader.check_id)421            results = [422                _outcome(423                    r.check_id,424                    CheckStatus.SKIP,425                    f"unmet dependency: runtime.startup ({source_problem})",426                )427                if r.check_id in invalidated428                else r429                for r in results430                if r.check_id != "runtime.startup"431            ]432            results.append(433                _outcome(434                    "runtime.startup",435                    CheckStatus.ERROR,436                    source_problem,437                )438            )439 440    report_type = (441        ValidationReportV2442        if wants_runtime or isinstance(manifest, NormalizedManifestV2)443        else ValidationReport444    )445    report = report_type(446        report_schema_version="2"447        if report_type is ValidationReportV2448        else REPORT_SCHEMA_VERSION,449        target=str(target),450        source_digest=digest_before,451        signature=signature,452        manifest=manifest,453        policy_version=policy.policy_version,454        lane=Lane.LOCAL,455        levels_run=levels,456        results=results,457        verdict=apply_policy(results, policy, Lane.LOCAL),458    )459    if artifacts_dir is not None:460        write_runtime_bundle(461            artifacts_dir,462            report,463            plan=plan,464            evidence=evidence,465            provider=inspection,466            cleanup=cleanup,467        )468    return report469