Skip to content

RFC 008: local and remote validation architecture - #1

Open
zkwentz wants to merge 17 commits into
mainfrom
rfc008-validation-stack
Open

RFC 008: local and remote validation architecture#1
zkwentz wants to merge 17 commits into
mainfrom
rfc008-validation-stack

Conversation

@zkwentz

@zkwentz zkwentz commented Jul 12, 2026

Copy link
Copy Markdown
Owner

What & Why

Environment authors need a fast local loop, an isolated remote option before publishing, actionable failure guidance, and a clear separation from richer independent Hub certification. This PR contains the complete RFC 008 implementation: spec-neutral planning/reporting, local and dedicated-Sandbox execution, strict staged push gating, a versioned unofficial author report, and the configuration/documentation needed to use it; future task-package dispatch remains tracked in huggingface/OpenEnv#898.

Risk & Review Guidance

  • Risk level: HIGH — 17 commits and 51 files touch untrusted-code execution, report integrity, CLI exit semantics, and publishing
  • Task class: feature / security / migration
  • Review focus: (1) src/openenv/validation/remote.py:242 and :551 for Sandbox/report trust boundaries; (2) src/openenv/cli/commands/push.py:342, :394, and :622 for exact-snapshot validation and upload ordering; (3) src/openenv/validation/specs/base.py:490 and planner.py:227 for future spec independence
  • Blast radius: A defect could accept forged or incomplete author evidence, expose credentials to submitted code, upload files not covered by the digest, or block Hub publishing.

This fork PR is intentionally large because it is the requested single-PR integration view. Human review should normally use the 17 smaller upstream draft PRs from #942 through #958.

Decisions & Alternatives

  • Use a normalized ValidationSubject behind trusted, pinned adapters — rejected coupling the planner directly to Harbor; served OpenEnv is initial, while one-shot/external specs can be added for Spec-agnostic task validation: openenv validate-task for external task.md packages (PostTrain Arena / BenchFlow) huggingface/OpenEnv#898.
  • Share one policy, check implementation, and report schema locally and remotely — rejected maintaining legacy CLI checks with different semantics.
  • Treat blocking skips as incomplete only in the strict publish profile — ordinary static/runtime/full profiles remain useful for iterative partial validation.
  • Use a fresh dedicated HF Sandbox and never forward the service token — rejected SandboxPool for uploaded cross-user code.
  • Separate the unprivileged subject UID from the trusted report writer — submitted code cannot write the protected report path.
  • Validate the transformed staging snapshot and bind its canonical digest before upload — rejected validating the live tree before excludes, Dockerfile relocation, and frontmatter changes.
  • Version the author report with the Space revision but never call it certified — the planned Hub service will independently produce signed security/artifact/training evidence.
  • ASSUMED: mandatory remote validation on Hub push is the intended rollout — custom registry pushes use the same strict profile locally.

Verification

  • PASS: PYTHONPATH=src:envs .venv/bin/python -m pytest tests/ -q -m 'not integration' — 1,559 passed, 114 skipped, 31 external integration tests deselected locally.
  • PASS: Full local run — 1,573 passed and 130 skipped; the sole failure was an unrelated deployed-Space TLS issuer error.
  • PASS: PYTHONPATH=src:envs .venv/bin/python -m pytest tests/test_cli/test_push.py tests/test_validation/test_remote.py -q — 70 passed after the final excluded-symlink refinement.
  • PASS: Fork CI — 7/7 checks passed on Python 3.11 and 3.12, including lint, docs, lock validation, and package smoke tests.
  • PASS: echo_env strict publish report — 20/22 criteria passed, 2 advisory skips, 16/16 blocking criteria passed, zero blocking skips, certified: false.
  • PASS: Ruff and Ruff formatting passed repository-wide.
  • NOT verified: A live HF Sandbox smoke test could not run because this machine has no HF token. Official coordinator, ACA, artifact, cross-host, signature, and reference-model lanes are not deployed.

Scope & Non-Goals

Breaking Changes / Migration Notes

  • Hub openenv push now requires HF authentication capable of creating a dedicated Sandbox and a fully passing strict publish profile.
  • New scaffolds include Harbor schema 1.1 task.toml; older environments need a truthful requirements envelope before publishing.
  • Custom registry pushes now run the strict publish profile locally.
  • Rollback is the prior push command path; author reports are additive files under .openenv/ and are explicitly non-certified.

Review Structure

The complete implementation is available as review-sized upstream drafts #942#958. This PR remains the requested one-piece integration view on the fork.

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The change adds RFC 008 shared validation contracts, Harbor task support, local and remote execution, dedicated sandbox security, strict publish gating, portable validation reports, CLI updates, documentation, templates, and extensive tests.

Changes

Validation and publish workflow

Layer / File(s) Summary
Validation contracts and spec adapters
src/openenv/validation/models.py, src/openenv/validation/specs/*, rfcs/*, task.toml templates
Adds shared validation models, policy contracts, Harbor schema 1.1 loading, OpenEnv spec detection, and generated task manifests.
Policy planning and execution
src/openenv/validation/checks.py, src/openenv/validation/planner.py, src/openenv/validation/executor.py, src/openenv/validation/serialization.py
Builds profile-specific plans, executes capability-aware checks, produces shared reports, and redacts unsafe evidence.
Local and remote validation
src/openenv/validation/local.py, src/openenv/validation/remote.py, src/openenv/validation/runtime_probe.py
Adds runtime probing, subprocess lifecycle handling, deterministic source snapshots, remote sandbox execution, and report contract validation.
Sandbox security
src/openenv/core/containers/runtime/hf_sandbox_provider.py, src/openenv/validation/security.py
Adds pooled/dedicated modes, secret handling, redirect protections, cleanup guarantees, and certification isolation checks.
CLI publish integration
src/openenv/cli/commands/validate.py, src/openenv/cli/commands/push.py
Adds profile and remote options, strict publish gates, staged snapshot binding, symlink rejection, and portable author reports.
Documentation and tests
README.md, docs/*, .claude/*, tests/*
Updates validation and publishing guidance and adds coverage for planning, reports, adapters, remote execution, sandbox security, and staging behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Developer
  participant ValidateCLI
  participant ValidationPlanner
  participant HFSandboxProvider
  participant ValidationReport
  Developer->>ValidateCLI: run publish validation
  ValidateCLI->>ValidationPlanner: build shared validation plan
  ValidateCLI->>HFSandboxProvider: upload revision and validator archives
  HFSandboxProvider-->>ValidateCLI: return validation report
  ValidateCLI->>ValidationReport: verify report and source digest
  ValidationReport-->>Developer: render publish gate result
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the PR’s main change: RFC 008 for local and remote validation architecture.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rfc008-validation-stack

Comment @coderabbitai help to get the list of available commands.

@zkwentz
zkwentz force-pushed the rfc008-validation-stack branch from c645b99 to e160992 Compare July 12, 2026 17:43

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openenv/cli/commands/push.py`:
- Around line 355-362: Update _validate_copy_source to apply
validation_source_path_allowed before rejecting symlinks, so candidates under
excluded trees such as .venv or node_modules are skipped. Only raise
typer.BadParameter for symlinks on paths allowed for staging or hashing,
preserving the existing error message and relative-path handling.

In `@src/openenv/validation/local.py`:
- Around line 507-531: Narrow the try/except around _launch_environment so only
environment startup failures are caught and converted to runtime_error. Move the
nested _run invocation outside that exception handler while preserving the
launched runtime_url path, ensuring exceptions from build_validation_plan or
execute_validation_plan propagate without triggering a second _run execution.
- Around line 571-583: Update the remediation rendering loop to use the redacted
safe_remediation values for the command and documentation branches: read the
command from safe_remediation when calling shlex.join and the URL from
safe_remediation when rendering Docs. Keep the existing path handling and output
structure unchanged.

In `@src/openenv/validation/specs/harbor.py`:
- Around line 64-70: Update _valid_name to use re.fullmatch with the existing
task-name pattern instead of re.match, preserving the existing ".." rejection
and error behavior so trailing-newline or multiline values cannot pass
validation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a0e41b6-47f1-4b7f-9a07-350b8b0d1dab

📥 Commits

Reviewing files that changed from the base of the PR and between 6823135 and c645b99.

📒 Files selected for processing (51)
  • .claude/docs/REPO_WALKTHROUGH.md
  • .claude/skills/generate-openenv-env/SKILL.md
  • .claude/skills/generate-openenv-env/assets/openenv_env_template/README.md
  • .claude/skills/generate-openenv-env/assets/openenv_env_template/task.toml
  • .claude/skills/generate-openenv-env/references/env-generation-checklist.md
  • README.md
  • docs/source/getting_started/contributing-envs.md
  • docs/source/getting_started/environment-builder.md
  • docs/source/guides/runtime-providers.md
  • docs/source/reference/cli.md
  • docs/source/reference/core.md
  • envs/echo_env/task.toml
  • pyproject.toml
  • rfcs/008-local-remote-validation.md
  • rfcs/010-echo-env-token-world-model.md
  • rfcs/README.md
  • src/openenv/cli/_validation.py
  • src/openenv/cli/commands/push.py
  • src/openenv/cli/commands/validate.py
  • src/openenv/cli/templates/openenv_env/README.md
  • src/openenv/cli/templates/openenv_env/task.toml
  • src/openenv/core/containers/runtime/hf_sandbox_provider.py
  • src/openenv/validation/__init__.py
  • src/openenv/validation/checks.py
  • src/openenv/validation/executor.py
  • src/openenv/validation/local.py
  • src/openenv/validation/models.py
  • src/openenv/validation/planner.py
  • src/openenv/validation/remote.py
  • src/openenv/validation/runtime_probe.py
  • src/openenv/validation/security.py
  • src/openenv/validation/serialization.py
  • src/openenv/validation/source_inspection.py
  • src/openenv/validation/specs/__init__.py
  • src/openenv/validation/specs/base.py
  • src/openenv/validation/specs/harbor.py
  • src/openenv/validation/specs/openenv.py
  • tests/test_cli/test_init.py
  • tests/test_cli/test_push.py
  • tests/test_cli/test_validate.py
  • tests/test_core/test_hf_sandbox_provider.py
  • tests/test_validation/__init__.py
  • tests/test_validation/_helpers.py
  • tests/test_validation/test_executor.py
  • tests/test_validation/test_guidance.py
  • tests/test_validation/test_harbor_spec.py
  • tests/test_validation/test_local.py
  • tests/test_validation/test_planner.py
  • tests/test_validation/test_remote.py
  • tests/test_validation/test_security.py
  • tests/test_validation/test_spec_registry.py

Comment thread src/openenv/cli/commands/push.py Outdated
Comment on lines +507 to +531
try:
with _launch_environment(source_path, timeout_s) as launched_url:
return _run(
target,
profile=selected_profile,
source_path=source_path,
spec_load=spec_load,
policy=policy,
runtime_url=launched_url,
runtime_error=None,
runtime_unavailable_reason=None,
timeout_s=timeout_s,
)
except Exception as exc:
return _run(
target,
profile=selected_profile,
source_path=source_path,
spec_load=spec_load,
policy=policy,
runtime_url=None,
runtime_error=type(exc).__name__,
runtime_unavailable_reason=None,
timeout_s=timeout_s,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Narrow the try/except to cover only environment launch, not the nested validation run.

The current except Exception wraps both _launch_environment and the nested _run(...) call. Any exception from build_validation_plan/execute_validation_plan inside the with block (unrelated to server startup) gets silently reclassified as a "runtime probe error" and triggers a second full re-execution of _run without a running server — masking the real cause and doubling work.

♻️ Proposed refactor to separate launch failures from execution failures
-    try:
-        with _launch_environment(source_path, timeout_s) as launched_url:
-            return _run(
-                target,
-                profile=selected_profile,
-                source_path=source_path,
-                spec_load=spec_load,
-                policy=policy,
-                runtime_url=launched_url,
-                runtime_error=None,
-                runtime_unavailable_reason=None,
-                timeout_s=timeout_s,
-            )
-    except Exception as exc:
-        return _run(
-            target,
-            profile=selected_profile,
-            source_path=source_path,
-            spec_load=spec_load,
-            policy=policy,
-            runtime_url=None,
-            runtime_error=type(exc).__name__,
-            runtime_unavailable_reason=None,
-            timeout_s=timeout_s,
-        )
+    launch_cm = _launch_environment(source_path, timeout_s)
+    try:
+        launched_url = launch_cm.__enter__()
+    except Exception as exc:
+        return _run(
+            target,
+            profile=selected_profile,
+            source_path=source_path,
+            spec_load=spec_load,
+            policy=policy,
+            runtime_url=None,
+            runtime_error=type(exc).__name__,
+            runtime_unavailable_reason=None,
+            timeout_s=timeout_s,
+        )
+    try:
+        return _run(
+            target,
+            profile=selected_profile,
+            source_path=source_path,
+            spec_load=spec_load,
+            policy=policy,
+            runtime_url=launched_url,
+            runtime_error=None,
+            runtime_unavailable_reason=None,
+            timeout_s=timeout_s,
+        )
+    finally:
+        launch_cm.__exit__(None, None, None)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openenv/validation/local.py` around lines 507 - 531, Narrow the
try/except around _launch_environment so only environment startup failures are
caught and converted to runtime_error. Move the nested _run invocation outside
that exception handler while preserving the launched runtime_url path, ensuring
exceptions from build_validation_plan or execute_validation_plan propagate
without triggering a second _run execution.

Comment on lines +571 to +583
for remediation in result.remediation:
safe_remediation = remediation.to_dict()
lines.append(f" Fix: {safe_remediation['message']}")
if remediation.argv:
lines.append(f" $ {shlex.join(remediation.argv)}")
elif remediation.path is not None:
destination = remediation.path
if remediation.pointer is not None:
destination += remediation.pointer
lines.append(f" Edit: {destination}")
elif remediation.url is not None:
lines.append(f" Docs: {remediation.url}")
if verbose and result.evidence:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## local.py around format_shared_validation_report\n'
sed -n '540,620p' src/openenv/validation/local.py

printf '\n## models.py around ValidationRemediation\n'
sed -n '1,260p' src/openenv/validation/models.py

Repository: zkwentz/OpenEnv

Length of output: 11746


Use safe_remediation for the command and docs branches. to_dict() already redacts argv and url, but this renderer still prints the raw values, which can leak sensitive remediation data into CLI output.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openenv/validation/local.py` around lines 571 - 583, Update the
remediation rendering loop to use the redacted safe_remediation values for the
command and documentation branches: read the command from safe_remediation when
calling shlex.join and the URL from safe_remediation when rendering Docs. Keep
the existing path handling and output structure unchanged.

Comment on lines +64 to +70
@field_validator("name")
@classmethod
def _valid_name(cls, value: str) -> str:
pattern = r"^[A-Za-z0-9_-][A-Za-z0-9._-]*/[A-Za-z0-9_-][A-Za-z0-9._-]*$"
if not re.match(pattern, value) or ".." in value:
raise ValueError("task name must use the org/name format")
return value

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Anchor the Harbor task-name check with fullmatch. re.match(..., "$") still accepts "org/name\n" because $ can match before a trailing newline. Use re.fullmatch(...) here so malformed multiline TOML values can’t pass the identity check.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 67-67: Regex pattern passed to re is built from a non-literal (variable, call, concatenation, or f-string) value. If that value is attacker-controlled it can introduce a malicious pattern with catastrophic backtracking (ReDoS). Use a hardcoded literal pattern, or validate/escape untrusted input with re.escape() and bound the regex complexity before compiling.
Context: re.match(pattern, value)
Note: [CWE-1333] Inefficient Regular Expression Complexity.

(redos-non-literal-regex-python)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openenv/validation/specs/harbor.py` around lines 64 - 70, Update
_valid_name to use re.fullmatch with the existing task-name pattern instead of
re.match, preserving the existing ".." rejection and error behavior so
trailing-newline or multiline values cannot pass validation.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/openenv/cli/commands/push.py (1)

273-277: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Require name to be a non-empty string.

Line 273 accepts truthy non-strings; e.g. a mapping can reach env_name.replace(...) during staging and crash before the publish gate.

Proposed fix
     env_name = manifest.get("name")
-    if not env_name:
-        raise typer.BadParameter("openenv.yaml must contain a 'name' field")
+    if not isinstance(env_name, str) or not env_name.strip():
+        raise typer.BadParameter(
+            "openenv.yaml must contain a non-empty string 'name' field"
+        )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openenv/cli/commands/push.py` around lines 273 - 277, Update the manifest
validation around env_name in the push command to require a non-empty string,
rejecting truthy non-string values before returning or staging. Preserve the
existing BadParameter message and return behavior for valid names.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/openenv/cli/commands/push.py`:
- Around line 273-277: Update the manifest validation around env_name in the
push command to require a non-empty string, rejecting truthy non-string values
before returning or staging. Preserve the existing BadParameter message and
return behavior for valid names.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7491f9e6-469d-4a22-b56a-0fdfb3586ead

📥 Commits

Reviewing files that changed from the base of the PR and between c645b99 and e160992.

📒 Files selected for processing (11)
  • .claude/skills/generate-openenv-env/assets/openenv_env_template/README.md
  • README.md
  • docs/source/getting_started/contributing-envs.md
  • docs/source/getting_started/environment-builder.md
  • docs/source/reference/cli.md
  • src/openenv/cli/commands/push.py
  • src/openenv/cli/commands/validate.py
  • src/openenv/cli/templates/openenv_env/README.md
  • src/openenv/validation/__init__.py
  • src/openenv/validation/remote.py
  • tests/test_cli/test_push.py
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/openenv/validation/init.py
  • README.md
  • docs/source/getting_started/contributing-envs.md
  • docs/source/reference/cli.md
  • docs/source/getting_started/environment-builder.md
  • src/openenv/cli/templates/openenv_env/README.md
  • src/openenv/cli/commands/validate.py
  • .claude/skills/generate-openenv-env/assets/openenv_env_template/README.md
  • src/openenv/validation/remote.py

)

provenance = _provenance(root_path, path, model.schema_version, document_digest)
if model.schema_version != "1.1":

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exact schema_version == "1.1" pin here couples the adapter to one snapshot of the Harbor schema. Suggest validating by major version and treating unknown minors/fields as advisory rather than a hard fail.

raise ValueError("report criterion metadata does not match the policy")


def run_remote_validation(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run_local_validation accepts spec_registry and policy, but run_remote_validation doesn't, so remote validation is pinned to the built-in openenv spec. Can the remote path accept a custom registry + policy too? Otherwise we can only validate a custom task.md adapter locally, not in an HF Sandbox.

effective_runtime_url = _normalize_runtime_url(effective_runtime_url)

if source_path is None:
if spec_id not in {None, "openenv"} or spec_registry is not None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL/runtime entrypoint rejects a custom registry. We can work around it by exposing our task as a standard OpenEnv server, so it's not blocking. But we want to confirm that the split (custom adapter for the static path, stock spec for the runtime path) is acceptable, or whether you intend to unify them.

process.wait(timeout=5.0)


def _server_command(env_path: Path, app: str, port: int) -> list[str]:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_server_command launches a single uvicorn app on one port, and _launch_environment yields one launched_url. Our served environments aren't a single app (the manifest declares multiple long-running services (auth/gmail/slack/gcal/... each on its own port) that the framework starts and gates the agent on via each service's /health.)

Is multi-service orchestration something the SERVED launch path could support, or is the expectation that an adapter collapses its environment behind a single OpenEnv-protocol endpoint? For us the latter is workable via our adaptor layer, just confirming the intended contract.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants