-
Notifications
You must be signed in to change notification settings - Fork 449
RFC 008 slice 1: walking skeleton — openenv validate end to end #1051
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,23 @@ | ||
| spec_version: 1 | ||
| name: echo_env | ||
| version: 0.1.0 | ||
| type: space | ||
| runtime: fastapi | ||
| app: server.app:app | ||
| port: 8000 | ||
| validation: | ||
| reward: | ||
| range: [0.0, 1.0] | ||
| oracle_tolerance: 0.0 | ||
| floor_margin: 0.5 | ||
| resources: | ||
| cpu: 1.0 | ||
| memory_mb: 1024 | ||
| disk_mb: 512 | ||
| episode_timeout_s: 60.0 | ||
| capabilities: | ||
| verifier: | ||
| kind: reward_channel | ||
| declared_tools: [echo_message, echo_with_length] | ||
| types: | ||
| tags: [demo] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,22 +3,39 @@ | |
| """ | ||
| OpenEnv validate command. | ||
|
|
||
| This module provides the 'openenv validate' command to check if environments | ||
| are properly configured for multi-mode deployment. | ||
| Local packages run through the RFC 008 validation pipeline (signature -> parse -> | ||
| grade -> severity policy -> report). Running servers are probed via --url (this | ||
| legacy probe folds into the pipeline's runtime level in a later slice). | ||
| """ | ||
|
|
||
| import json | ||
| from pathlib import Path | ||
| from typing import Annotated | ||
|
|
||
| import typer | ||
| from openenv.cli._validation import ( | ||
| build_local_validation_json_report, | ||
| format_validation_report, | ||
| get_deployment_modes, | ||
| validate_multi_mode_deployment, | ||
| validate_running_environment, | ||
| from openenv.cli._validation import validate_running_environment | ||
| from openenv.validation import ( | ||
| Level, | ||
| load_policy, | ||
| PolicyError, | ||
| SignatureError, | ||
| UnsupportedPackageError, | ||
| ValidationReport, | ||
| Verdict, | ||
| ) | ||
| from openenv.validation.runner import run_validation | ||
|
|
||
| # Exit-code contract (RFC 008 §8): | ||
| EXIT_PASS = 0 # verdict PASS or WARN | ||
| EXIT_FAIL = 1 # verdict FAIL | ||
| EXIT_UNSUPPORTED = 2 # SignatureError / UnsupportedPackageError | ||
| EXIT_INTERNAL = 3 # internal error | ||
|
|
||
| _LEVELS = { | ||
| "static": Level.STATIC, | ||
| "runtime": Level.RUNTIME, | ||
| "semantic": Level.SEMANTIC, | ||
| } | ||
|
|
||
|
|
||
| def _looks_like_url(value: str) -> bool: | ||
|
|
@@ -27,12 +44,32 @@ def _looks_like_url(value: str) -> bool: | |
| return candidate.startswith("http://") or candidate.startswith("https://") | ||
|
|
||
|
|
||
| def _render_report(report: ValidationReport) -> str: | ||
| """Human-readable report summary.""" | ||
| lines = [ | ||
| f"Validation report for {report.target} (signature: {report.signature.value})", | ||
| f" policy {report.policy_version} · levels run: " | ||
| + ", ".join(level.name.lower() for level in report.levels_run), | ||
| ] | ||
| for result in report.results: | ||
| lines.append( | ||
| f" {result.status.value.upper():5s} {result.check_id} ({result.duration_s:.2f}s)" | ||
| ) | ||
| if result.status.value in ("fail", "error", "skip"): | ||
| for line in result.evidence: | ||
| lines.append(f" {line}") | ||
| if result.remediation: | ||
| lines.append(f" remediation: {result.remediation}") | ||
| lines.append(f"Verdict: {report.verdict.value.upper()}") | ||
| return "\n".join(lines) | ||
|
|
||
|
|
||
| def validate( | ||
| target: Annotated[ | ||
| str | None, | ||
| typer.Argument( | ||
| help=( | ||
| "Path to the environment directory (default: current directory) " | ||
| "Path to the package directory (default: current directory) " | ||
| "or a running OpenEnv URL (http://... or https://...)" | ||
| ), | ||
| ), | ||
|
|
@@ -44,54 +81,67 @@ def validate( | |
| help="Validate a running OpenEnv server by base URL (e.g. http://localhost:8000)", | ||
| ), | ||
| ] = None, | ||
| json_output: Annotated[ | ||
| level: Annotated[ | ||
| str, | ||
| typer.Option( | ||
| "--level", | ||
| help="Validation level ceiling: static, runtime, or semantic", | ||
| ), | ||
| ] = "semantic", | ||
| skip_build: Annotated[ | ||
| bool, | ||
| typer.Option( | ||
| "--json", | ||
| help="Output local validation report as JSON (runtime validation is JSON by default)", | ||
| "--skip-build", | ||
| help="Skip the image build; build-dependent checks are SKIPped with a reason", | ||
| ), | ||
| ] = False, | ||
| policy_version: Annotated[ | ||
| str, | ||
| typer.Option("--policy", help="Severity policy version to apply"), | ||
| ] = "v1", | ||
| json_output: Annotated[ | ||
| bool, | ||
| typer.Option("--json", help="Print the validation report as JSON"), | ||
| ] = False, | ||
| output: Annotated[ | ||
| Path | None, | ||
| typer.Option("--output", help="Write the JSON report to a file"), | ||
| ] = None, | ||
| timeout: Annotated[ | ||
| float, | ||
| typer.Option( | ||
| "--timeout", | ||
| help="HTTP timeout in seconds for runtime validation", | ||
| help="HTTP timeout in seconds for --url runtime validation", | ||
| min=0.1, | ||
| ), | ||
| ] = 5.0, | ||
| verbose: Annotated[ | ||
| bool, typer.Option("--verbose", "-v", help="Show detailed information") | ||
| ] = False, | ||
| ) -> None: | ||
| """ | ||
| Validate local environments and running OpenEnv servers. | ||
| Validate a local package or a running OpenEnv server. | ||
|
|
||
| Local validation checks if an environment is properly configured with: | ||
| - Required files (pyproject.toml, openenv.yaml, server/app.py, etc.) | ||
| - Docker deployment support | ||
| - uv run server capability | ||
| - python -m module execution | ||
| Local validation detects the package format by its well-known file (the | ||
| formats this build can parse; currently openenv.yaml), parses it into the | ||
| normalized manifest, runs the applicable graders up to the requested level, | ||
| applies the severity policy, and emits a report. | ||
|
|
||
| Runtime validation checks if a live OpenEnv server conforms to the | ||
| versioned runtime API contract and returns a criteria-based JSON report. | ||
| Exit codes: 0 pass/warn - 1 fail - 2 unrecognized/unsupported package - 3 | ||
| internal error. | ||
|
|
||
| Examples: | ||
|
|
||
| ```bash | ||
| # Validate current directory (recommended) | ||
| $ cd my_env | ||
| $ openenv validate | ||
| ```bash | ||
| # Validate the current directory up to the semantic level | ||
| openenv validate | ||
|
|
||
| # Validate a running environment and return JSON criteria | ||
| $ openenv validate --url http://localhost:8000 | ||
| $ openenv validate https://my-env.hf.space | ||
| # Fast inner loop: static checks only, no image build | ||
| openenv validate envs/echo_env --level static --skip-build | ||
|
|
||
| # Validate with detailed output | ||
| $ openenv validate --verbose | ||
| # Machine-readable report | ||
| openenv validate envs/echo_env --json | ||
|
|
||
| # Validate specific environment | ||
| $ openenv validate envs/echo_env | ||
| ``` | ||
| # Probe a running server (legacy runtime probe) | ||
| openenv validate --url http://localhost:8000 | ||
| ``` | ||
| """ | ||
| runtime_target = url | ||
| if ( | ||
|
|
@@ -103,95 +153,62 @@ def validate( | |
| "Error: Cannot combine a local path argument with --url runtime validation", | ||
| err=True, | ||
| ) | ||
| raise typer.Exit(1) | ||
| raise typer.Exit(EXIT_FAIL) | ||
|
|
||
| if target is not None and _looks_like_url(target): | ||
| if runtime_target is not None and runtime_target != target: | ||
| typer.echo( | ||
| "Error: Conflicting runtime targets provided via argument and --url", | ||
| err=True, | ||
| ) | ||
| raise typer.Exit(1) | ||
| raise typer.Exit(EXIT_FAIL) | ||
| runtime_target = target | ||
|
|
||
| if runtime_target is not None: | ||
| try: | ||
| report = validate_running_environment(runtime_target, timeout_s=timeout) | ||
| except ValueError as exc: | ||
| typer.echo(f"Error: {exc}", err=True) | ||
| raise typer.Exit(1) from exc | ||
| raise typer.Exit(EXIT_FAIL) from exc | ||
|
|
||
| typer.echo(json.dumps(report, indent=2)) | ||
| if not report.get("passed", False): | ||
| raise typer.Exit(1) | ||
| raise typer.Exit(EXIT_FAIL) | ||
| return | ||
|
|
||
| # Determine environment path (default to current directory) | ||
| if target is None: | ||
| env_path_obj = Path.cwd() | ||
| else: | ||
| env_path_obj = Path(target) | ||
|
|
||
| if not env_path_obj.exists(): | ||
| typer.echo(f"Error: Path does not exist: {env_path_obj}", err=True) | ||
| raise typer.Exit(1) | ||
|
|
||
| if not env_path_obj.is_dir(): | ||
| typer.echo(f"Error: Path is not a directory: {env_path_obj}", err=True) | ||
| raise typer.Exit(1) | ||
|
|
||
| # Check for openenv.yaml to confirm this is an environment directory | ||
| openenv_yaml = env_path_obj / "openenv.yaml" | ||
| if not openenv_yaml.exists(): | ||
| if level not in _LEVELS: | ||
| typer.echo( | ||
| f"Error: Not an OpenEnv environment directory (missing openenv.yaml): {env_path_obj}", | ||
| f"Error: unknown level {level!r}; expected one of {sorted(_LEVELS)}", | ||
| err=True, | ||
| ) | ||
| typer.echo( | ||
| "Hint: Run this command from the environment root directory or specify the path", | ||
| err=True, | ||
| raise typer.Exit(EXIT_INTERNAL) | ||
|
|
||
| package_root = Path(target) if target is not None else Path.cwd() | ||
| if not package_root.is_dir(): | ||
| typer.echo(f"Error: not a package directory: {package_root}", err=True) | ||
| raise typer.Exit(EXIT_UNSUPPORTED) | ||
|
|
||
| try: | ||
| validation_report = run_validation( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tier 1 (test regression): this rewrite replaces the old local-validation path ( |
||
| package_root, | ||
| max_level=_LEVELS[level], | ||
| skip_build=skip_build, | ||
| policy=load_policy(policy_version), | ||
| ) | ||
| raise typer.Exit(1) | ||
|
|
||
| env_name = env_path_obj.name | ||
| if env_name.endswith("_env"): | ||
| base_name = env_name[:-4] | ||
| else: | ||
| base_name = env_name | ||
|
|
||
| # Run validation | ||
| is_valid, issues = validate_multi_mode_deployment(env_path_obj) | ||
| modes = get_deployment_modes(env_path_obj) | ||
|
|
||
| except (SignatureError, UnsupportedPackageError) as exc: | ||
| typer.echo(f"Error: {exc}", err=True) | ||
| raise typer.Exit(EXIT_UNSUPPORTED) from exc | ||
| except PolicyError as exc: | ||
| typer.echo(f"Internal error: {exc}", err=True) | ||
| raise typer.Exit(EXIT_INTERNAL) from exc | ||
|
|
||
| report_json = validation_report.model_dump_json(indent=2) | ||
| if output is not None: | ||
| output.write_text(report_json + "\n") | ||
| if json_output: | ||
| report = build_local_validation_json_report( | ||
| env_name=base_name, | ||
| env_path=env_path_obj, | ||
| is_valid=is_valid, | ||
| issues=issues, | ||
| deployment_modes=modes if verbose else None, | ||
| ) | ||
| typer.echo(json.dumps(report, indent=2)) | ||
| if not is_valid: | ||
| raise typer.Exit(1) | ||
| return | ||
| typer.echo(report_json) | ||
| else: | ||
| typer.echo(_render_report(validation_report)) | ||
|
|
||
| # Show validation report | ||
| report = format_validation_report(base_name, is_valid, issues) | ||
| typer.echo(report) | ||
|
|
||
| # Show deployment modes if verbose | ||
| if verbose: | ||
| typer.echo("\nSupported deployment modes:") | ||
| for mode, supported in modes.items(): | ||
| status = "[YES]" if supported else "[NO]" | ||
| typer.echo(f" {status} {mode}") | ||
|
|
||
| if is_valid: | ||
| typer.echo("\nUsage examples:") | ||
| typer.echo(f" cd {env_path_obj.name} && uv run server") | ||
| typer.echo(f" cd {env_path_obj.name} && openenv build") | ||
| typer.echo(f" cd {env_path_obj.name} && openenv push") | ||
|
|
||
| if not is_valid: | ||
| raise typer.Exit(1) | ||
| if validation_report.verdict is Verdict.FAIL: | ||
| raise typer.Exit(EXIT_FAIL) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| """Static-level (L1) graders.""" | ||
|
|
||
| from .manifest import StaticManifestGrader | ||
|
|
||
| __all__ = ["StaticManifestGrader"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tier 1 (CI-blocking): this rewrite replaces the old local-validation path (
validate_multi_mode_deployment/build_local_validation_json_report,[OK]output,main()-guard + dependency checks) with the RFC 008 pipeline, but the tests that assert the old behavior intests/test_cli/test_validate.pywere not updated. 8 of them now fail on HEAD (all 12 pass on the base commit). Please delete or rewrite the obsolete local-path tests to match the new pipeline contract — the--url/runtime tests and the mixed-path guard test still pass and should be kept.