Skip to content

feat(cli): add openenv serve for local uvicorn#668

Open
false200 wants to merge 2 commits into
huggingface:mainfrom
false200:feat/cli-openenv-serve
Open

feat(cli): add openenv serve for local uvicorn#668
false200 wants to merge 2 commits into
huggingface:mainfrom
false200:feat/cli-openenv-serve

Conversation

@false200
Copy link
Copy Markdown

Related to meta-pytorch/OpenEnv#613

Adds an openenv serve command that reads openenv.yaml (app, port, runtime), validates the env layout, sets cwd and sys.path like a normal env run, optionally prepends the OpenEnv repo src/ when the env lives under a clone, and starts the app with uvicorn. Documents openenv serve . in the README. Adds a minimal envs/echo_env/models.py so validate_env_structure passes. Includes CLI tests (mocked uvicorn + optional subprocess /health on echo). Includes small import-order fixes in test_grid_world.py and test_julia_env.py so usort/ruff format stay stable.

Summary

Introduces openenv serve as the supported way to run a FastAPI OpenEnv app locally from an environment directory, aligned with manifest-driven deployment and existing validation.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • New environment
  • Refactoring

Alignment Checklist

Before submitting, verify:

  • I have read .claude/docs/PRINCIPLES.md and this PR aligns with our principles
  • I have checked .claude/docs/INVARIANTS.md and no invariants are violated
  • I have run /pre-submit-pr (or bash .claude/hooks/lint.sh and tests) and addressed all issues

(Dev-only CLI; does not change WebSocket/MCP contracts or agent-facing APIs.)

RFC Status

  • Not required (bug fix, docs, minor refactoring)
  • RFC exists: #___
  • RFC needed (will create before merge)

Test Plan

  1. Lint (matches CI):
    uv run usort format src/ tests/ && uv run ruff format src/ tests/ → no diff;
    uv run usort check src/ tests/ && uv run ruff check src/ tests/

  2. Targeted tests:
    PYTHONPATH=src:envs uv run pytest tests/test_cli/test_serve.py -v

  3. Manual: from envs/echo_env (or any valid env with runtime: fastapi):
    openenv serve . --host 127.0.0.1 --port <port>GET /health returns 200.

Claude Code Review

N/A

- Load app/host/port from openenv.yaml and run uvicorn with env cwd on sys.path

- Prepend repo src when the env path sits under an OpenEnv clone

- Register serve on the Typer CLI; document usage in README

- Add echo_env models.py stub required by validate_env_structure

- Add tests for serve (mocked uvicorn + echo /health subprocess)

- Stabilize usort/ruff import blocks in grid_world and julia env tests
@meta-cla
Copy link
Copy Markdown

meta-cla Bot commented May 11, 2026

Hi @false200!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 11, 2026

Greptile Summary

This PR implements openenv serve, promoting the command from a placeholder stub to a working local uvicorn launcher that reads openenv.yaml (app, port, runtime), validates the env layout, adjusts sys.path and CWD, and starts the FastAPI app. Supporting additions include a stub models.py for echo_env, unit + integration tests, and a README entry.

  • serve.py: Parses the YAML manifest, validates runtime is fastapi, resolves the repo src/ heuristic, mutates sys.path and CWD in-process, then delegates to uvicorn.run — two issues: the global state mutations contaminate any in-process caller (including the CliRunner-based unit tests), and the integration test has a deadlock path when the health check deadline is exceeded.
  • Alignment concern: openenv serve runs environment code directly on the host, bypassing the Docker container-isolation invariant in INVARIANTS.md; this trade-off was not discussed in an RFC and warrants explicit sign-off from @darktex.
  • Import-order fixes in test_grid_world.py and test_julia_env.py are cosmetic and correct.

Confidence Score: 3/5

Not safe to merge without fixing the integration test deadlock and the in-process global-state mutations that corrupt test isolation.

The serve command calls os.chdir and sys.path.insert before handing off to uvicorn; since CliRunner runs in-process, these mutations persist across every subsequent unit test in the suite. Separately, the integration test calls proc.stdout.read() on a live subprocess pipe before proc.terminate(), hanging the suite indefinitely whenever the server fails to start within the deadline.

src/openenv/cli/commands/serve.py (global-state mutations) and tests/test_cli/test_serve.py (missing CWD/sys.path teardown in unit tests, deadlock in integration test).

Important Files Changed

Filename Overview
src/openenv/cli/commands/serve.py Implements openenv serve with YAML manifest parsing and uvicorn launch; global os.chdir + sys.path mutations executed in-process will contaminate test runners and any process that invokes this function without subprocess isolation.
tests/test_cli/test_serve.py New CLI tests for serve; unit tests use in-process CliRunner and don't restore CWD/sys.path, and the integration test has a deadlock when the health-check deadline is exceeded before proc.terminate() is called.
src/openenv/cli/main.py Minor reformatting of the serve command registration; help text updated from placeholder to accurate description — no logic changes.
envs/echo_env/models.py Adds a stub models.py so validate_env_structure passes for echo_env; intentionally empty beyond the docstring.
README.md Documents the new openenv serve . invocation alongside the existing uv run server command — accurate and minimal.
tests/envs/test_grid_world.py Removes a stale inline comment to fix import ordering for usort/ruff; no logic changes.
tests/envs/test_julia_env.py Removes two stale inline comments to fix import ordering for usort/ruff; no logic changes.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI as openenv CLI
    participant Serve as serve()
    participant FS as Filesystem
    participant UV as uvicorn

    User->>CLI: openenv serve env_path [--port] [--host]
    CLI->>Serve: invoke serve()
    Serve->>FS: validate_env_structure(env_path)
    FS-->>Serve: OK / FileNotFoundError
    Serve->>FS: open openenv.yaml - yaml.safe_load()
    FS-->>Serve: manifest dict
    Serve->>Serve: validate app, runtime fields
    Serve->>Serve: _find_repo_src_for_openenv() - sys.path.insert()
    Serve->>Serve: sys.path.insert(env_root)
    Serve->>Serve: os.chdir(env_root)
    Serve->>UV: uvicorn.run(app_spec, host, port, reload)
    UV-->>User: HTTP server listening
Loading

Comments Outside Diff (1)

  1. src/openenv/cli/commands/serve.py, line 1-30 (link)

    P2 ALIGNMENT FLAG: openenv serve bypasses Docker container isolation

    INVARIANTS.md §Security Invariants states: "Environments run in isolated Docker containers. Containers must not have access to host filesystem." openenv serve runs the FastAPI app directly in the host Python process — no container, no filesystem isolation, no network namespace. This trade-off was not discussed in an RFC and warrants explicit sign-off.

    • Principle at stake: Container isolation (INVARIANTS.md §Security)
    • The concern: Running env code on the host bypasses isolation guarantees and could normalise "no-container" deployments
    • Suggested reviewer: @darktex
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/openenv/cli/commands/serve.py
    Line: 1-30
    
    Comment:
    **ALIGNMENT FLAG: `openenv serve` bypasses Docker container isolation**
    
    INVARIANTS.md §Security Invariants states: *"Environments run in isolated Docker containers. Containers must not have access to host filesystem."* `openenv serve` runs the FastAPI app directly in the host Python process — no container, no filesystem isolation, no network namespace. This trade-off was not discussed in an RFC and warrants explicit sign-off.
    
    - **Principle at stake**: Container isolation (INVARIANTS.md §Security)
    - **The concern**: Running env code on the host bypasses isolation guarantees and could normalise "no-container" deployments
    - **Suggested reviewer**: `@darktex`
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
tests/test_cli/test_serve.py:124-126
**`proc.stdout.read()` deadlocks when the server never becomes healthy**

When the 60-second deadline expires with `ok = False`, line 125 calls `proc.stdout.read()` on a pipe connected to a still-running process. Because nothing has terminated the subprocess yet (`proc.terminate()` is in the `finally` block, which only executes *after* control leaves the `try` body), the read blocks indefinitely — `pytest.fail` is never reached and neither is the `finally` cleanup. Terminate the process before reading its output.

### Issue 2 of 3
src/openenv/cli/commands/serve.py:107-117
**Global process-state mutation breaks in-process test isolation**

`sys.path.insert` and `os.chdir` are irreversible global side effects. Because `typer.testing.CliRunner` runs the command in the *same* process (not a subprocess), every unit test that calls `runner.invoke` with `serve` will permanently move the test runner's CWD to `env_root` and prepend entries to `sys.path`. Tests that depend on the original CWD or a clean path (including subsequent tests in this very module) can silently break or import the wrong module version. The cleanest fix is to exec a child process so all global-state mutations are isolated, or add an autouse fixture that saves and restores `os.getcwd()` and `sys.path`.

### Issue 3 of 3
src/openenv/cli/commands/serve.py:1-30
**ALIGNMENT FLAG: `openenv serve` bypasses Docker container isolation**

INVARIANTS.md §Security Invariants states: *"Environments run in isolated Docker containers. Containers must not have access to host filesystem."* `openenv serve` runs the FastAPI app directly in the host Python process — no container, no filesystem isolation, no network namespace. This trade-off was not discussed in an RFC and warrants explicit sign-off.

- **Principle at stake**: Container isolation (INVARIANTS.md §Security)
- **The concern**: Running env code on the host bypasses isolation guarantees and could normalise "no-container" deployments
- **Suggested reviewer**: `@darktex`

Reviews (1): Last reviewed commit: "feat(cli): add openenv serve for local u..." | Re-trigger Greptile

Comment on lines +124 to +126
if not ok:
out = proc.stdout.read() if proc.stdout else ""
pytest.fail(f"/health never OK (last error={last_exc!r}): {out}")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 proc.stdout.read() deadlocks when the server never becomes healthy

When the 60-second deadline expires with ok = False, line 125 calls proc.stdout.read() on a pipe connected to a still-running process. Because nothing has terminated the subprocess yet (proc.terminate() is in the finally block, which only executes after control leaves the try body), the read blocks indefinitely — pytest.fail is never reached and neither is the finally cleanup. Terminate the process before reading its output.

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/test_cli/test_serve.py
Line: 124-126

Comment:
**`proc.stdout.read()` deadlocks when the server never becomes healthy**

When the 60-second deadline expires with `ok = False`, line 125 calls `proc.stdout.read()` on a pipe connected to a still-running process. Because nothing has terminated the subprocess yet (`proc.terminate()` is in the `finally` block, which only executes *after* control leaves the `try` body), the read blocks indefinitely — `pytest.fail` is never reached and neither is the `finally` cleanup. Terminate the process before reading its output.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in b56360c commit

Comment on lines +107 to +117
repo_src = _find_repo_src_for_openenv(env_path_obj)
if repo_src is not None:
repo_src_str = str(repo_src.resolve())
if repo_src_str not in sys.path:
sys.path.insert(0, repo_src_str)

env_root = str(env_path_obj.resolve())
if env_root not in sys.path:
sys.path.insert(0, env_root)

os.chdir(env_root)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Global process-state mutation breaks in-process test isolation

sys.path.insert and os.chdir are irreversible global side effects. Because typer.testing.CliRunner runs the command in the same process (not a subprocess), every unit test that calls runner.invoke with serve will permanently move the test runner's CWD to env_root and prepend entries to sys.path. Tests that depend on the original CWD or a clean path (including subsequent tests in this very module) can silently break or import the wrong module version. The cleanest fix is to exec a child process so all global-state mutations are isolated, or add an autouse fixture that saves and restores os.getcwd() and sys.path.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/openenv/cli/commands/serve.py
Line: 107-117

Comment:
**Global process-state mutation breaks in-process test isolation**

`sys.path.insert` and `os.chdir` are irreversible global side effects. Because `typer.testing.CliRunner` runs the command in the *same* process (not a subprocess), every unit test that calls `runner.invoke` with `serve` will permanently move the test runner's CWD to `env_root` and prepend entries to `sys.path`. Tests that depend on the original CWD or a clean path (including subsequent tests in this very module) can silently break or import the wrong module version. The cleanest fix is to exec a child process so all global-state mutations are isolated, or add an autouse fixture that saves and restores `os.getcwd()` and `sys.path`.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in b56360c commit.

- Autouse fixture restores cwd and sys.path after in-process CliRunner invokes

- Terminate subprocess before reading stdout when /health deadline exceeded (Greptile P1)
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label May 11, 2026
@false200
Copy link
Copy Markdown
Author

Issue 3: host vs Docker

Hi @Darktex, openenv serve is just local dev (same idea as running uvicorn from the env dir), not “skip Docker in prod.” For isolation / real deployments, I’m still treating Docker / Spaces as the right path per INVARIANTS.

Happy to add a one line note in serve()’s docstring or README if you want that spelled out in tree.

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

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant