Skip to content

fix(envs): declare app paths the server can actually be imported under - #1068

Open
k21993 wants to merge 3 commits into
huggingface:mainfrom
k21993:fix/awm-manifest-app-target
Open

fix(envs): declare app paths the server can actually be imported under#1068
k21993 wants to merge 3 commits into
huggingface:mainfrom
k21993:fix/awm-manifest-app-target

Conversation

@k21993

@k21993 k21993 commented Aug 6, 2026

Copy link
Copy Markdown

Summary

Seven manifests declare app: server.app:app while their server package imports from ..models. Under that name server is top-level, so the relative import climbs past it and the server dies with ImportError: attempted relative import beyond top-level package.

Only the cloud providers read this field :ModalProvider._discover_server_cmd and its Daytona twin run cd <env root> && python -m uvicorn <app>: so the breakage is invisible locally, where docker run uses the Dockerfile CMD instead. Each manifest now names the same target its own Dockerfile already launches.

Scope note: this PR originally changed one manifest to server.app:app. @cursor correctly caught that this was wrong. That value cannot import for this env and the investigation showed the bug affects seven envs, not one. History rewritten accordingly; see the review thread.

Env Was Now
agent_world_model_env envs.agent_world_model_env.server.app:app agent_world_model_env.server.app:app
carla_env server.app:app carla_env.server.app:app
chess_env server.app:app chess_env.server.app:app
finqa_env server.app:app finqa_env.server.app:app
grid_world_env server.app:app envs.grid_world_env.server.app:app
pelican_svg_env server.app:app pelican_svg_env.server.app:app
wildfire_env server.app:app wildfire_env.server.app:app

grid_world_env differs because it builds from the repo root (COPY envs/grid_world_env/ /app/envs/grid_world_env/, ENV PYTHONPATH=/app), so envs. is the correct prefix there — its Dockerfile already says so.

Envs that declare server.app:app and import cleanly under it (echo_env, chat_env, repl_env, …) are untouched.

Type of Change

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

Alignment Checklist

  • 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 bash .claude/hooks/lint.sh and tests and addressed all issues

To be precise on the third box: lint.sh fails on this branch, but it fails identically on an untouched mainruff format envs/ --check reports 55 unformatted .py files there, none touched here. The files in this diff pass ruff format --check, ruff check, and usort check. Unrelated reformatting was deliberately kept out.

Separately, lint.sh leaves envs/**/README.md dirty on every run: line 24 runs ruff format in place (this ruff version formats Python blocks inside Markdown), but the cleanup at line 27 filters to *.py, so the Markdown is never restored. Happy to send that one-line fix separately.

RFC Status

  • Not required (bug fix, docs, minor refactoring)

RFC 002's cloud sandbox provider section maps "run a process inside the sandbox" to a provider-internal start command; this repairs that path rather than changing it.

Test Plan

tests/envs/test_manifest_app_targets.py checks two things per manifest: the declared module exists, and it is importable under the name declared. The second is what the first version of this PR missed — server/app.py exists whether or not server.app can be imported.

It does not import the modules. That would pull in playwright, carla, dm_control and the rest of the optional-dependency tail, so the module would skip on exactly the CI machines that should be guarding this — which is how this drift went unnoticed. Instead it walks the reachable import graph with ast, tracking each module's depth in the declared namespace and flagging any relative import whose level exceeds it. Imports wrapped in try/except ImportError are not counted, since that fallback is how most envs already support both layouts.

Validation against real interpreter behaviour. The static check was cross-checked against actually running import <declared> from each env directory, for all 35 manifests: no false positives and no false negatives on every env whose dependencies are installed locally. Two envs (chess_env, pelican_svg_env) could only be confirmed statically because chess / resvg_py are not installed; both already use the packaged form in their Dockerfile, which corroborates.

Reproduce the failure on main:

$ cd envs/agent_world_model_env
$ python -c "import server.app"
  File ".../server/awm_environment.py", line 19, in <module>
    from ..models import AWMListToolsObservation, AWMObservation
ImportError: attempted relative import beyond top-level package

$ cd ../.. && PYTHONPATH=src:envs python -c "import agent_world_model_env.server.app"   # the fixed value

Suite: 1601 passed, 133 skipped (baseline on main is 1530 passed, 133 skipped). python scripts/sync_env_docs.py --check passes.

The three legacy manifests with no app field (browsergym_env, coding_env, julia_env) are skipped rather than failed. Migrating those to the current schema touches more envs, raises back-compat questions, and coding_env has its own build layout — separate PR.

Claude Code Review

/alignment-review: 0 Tier 1 issues, 0 principle conflicts, 0 RFC conflicts. Automated checks report only pre-existing findings on main (unformatted envs/*.py; console.print in src/openenv/cli/), none in this diff.


Note

Medium Risk
Changes only manifest strings and tests, but wrong app values previously broke cloud sandboxes for affected envs; fixes are low-risk and aligned with existing Dockerfile CMDs.

Overview
Fixes Modal/Daytona startup for seven envs by aligning each openenv.yaml app value with the module path uvicorn can actually import (cloud runs python -m uvicorn <app> from the manifest, not the Dockerfile CMD).

Most envs move from server.app:app to <env>.server.app:app so parent-relative imports like from ..models resolve. agent_world_model_env drops the invalid envs. prefix; grid_world_env keeps envs.grid_world_env.server.app:app to match its repo-root image layout.

Adds tests/envs/test_manifest_app_targets.py, which parametrizes all manifests with an app field: checks the module exists, and uses AST (not runtime imports) to flag relative imports that escape the declared package root, with layout-specific rules for the envs. prefix.

Reviewed by Cursor Bugbot for commit 0b3e94c. Bugbot is set up for automated code reviews on this repo. Configure here.

The manifest declared `app: envs.agent_world_model_env.server.app:app`, the
only one of 35 envs not using `server.app:app`. The cloud providers read this
field verbatim and run `cd /app/env && python -m uvicorn <app>`, so this env
fails to start on Modal and Daytona with ModuleNotFoundError: images are built
with the env directory as context, and the installed dist maps
`agent_world_model_env` to `.`, so no `envs` package exists in either layout.

Add a contract test resolving every manifest's app target against the env
directory on disk. It does not import the modules: that would pull in
playwright, carla and the rest of the optional-dependency tail and skip on
exactly the CI machines that should be guarding this.
Comment thread envs/agent_world_model_env/openenv.yaml Outdated
Seven manifests declared `app: server.app:app` while their server package
imports `from ..models`. Under that name `server` is top-level, so the relative
import climbs past it and the server dies with `ImportError: attempted relative
import beyond top-level package`. Only the cloud providers read this field --
`ModalProvider._discover_server_cmd` and its Daytona twin run
`cd <env root> && python -m uvicorn <app>` -- so the breakage is invisible
locally, where `docker run` uses the Dockerfile CMD instead.

Each manifest now names the same target its own Dockerfile launches: the env's
distribution package for six of them, and `envs.grid_world_env...` for
grid_world_env, which builds from the repo root.

The contract test now checks importability rather than mere file existence,
walking the reachable import graph with `ast`. It does not import the modules:
that would pull in playwright, carla and the rest of the optional-dependency
tail and skip on exactly the CI machines that should be guarding this. Imports
wrapped in a try/except ImportError are not counted, since that fallback is how
most envs already support both layouts.

Validated against real interpreter behaviour across all 35 manifests: no false
positives and no false negatives on every env whose dependencies are installed.
@k21993 k21993 changed the title fix(agent_world_model_env): point manifest app at the real server module fix(envs): declare app paths the server can actually be imported under Aug 6, 2026
@k21993

k21993 commented Aug 6, 2026

Copy link
Copy Markdown
Author

@cursor Good catch — you were right, and the fix has been rewritten.

Confirmed your diagnosis by reproduction:

$ cd envs/agent_world_model_env && python -c "import server.app"
  File ".../server/awm_environment.py", line 19, in <module>
    from ..models import AWMListToolsObservation, AWMObservation
ImportError: attempted relative import beyond top-level package

server.app:app was wrong for exactly the reason you gave: no ImportError fallback, so the relative import escapes. The manifest now uses agent_world_model_env.server.app:app, matching the Dockerfile CMD you pointed at.

Two follow-ons from your review:

The bug is not one env. Auditing all 35 manifests turned up seven with the same defect (carla_env, chess_env, finqa_env, grid_world_env, pelican_svg_env, wildfire_env alongside agent_world_model_env). All seven are fixed here. grid_world_env needed envs.grid_world_env... rather than the package form, because it builds from the repo root.

The original test could not have caught this. It only checked that the module file existed on disk, which server/app.py does. It now checks importability under the declared name by walking the reachable import graph with ast — still without importing anything, so optional deps like playwright and carla stay out of CI. Your point about the ImportError fallback is encoded directly: guarded imports are not counted, which is what keeps chat_env and reasoning_gym_env correctly passing on the flat form.

The static check was cross-validated against real import behaviour across all 35 envs: no false positives, no false negatives on every env whose deps are installed.

@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

Unable to authenticate your request. Please make sure to connect your GitHub account to Cursor. Go to Cursor

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4cee358. Configure here.

Comment thread tests/envs/test_manifest_app_targets.py Outdated
@bot-ci-comment

bot-ci-comment Bot commented Aug 6, 2026

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

`_resolve` fell back to the repo root unconditionally, and the source tree
always contains `envs/<env>/server/app.py`. Every env therefore accepted an
`envs.` prefix -- including the original
`envs.agent_world_model_env.server.app:app`, the exact value this suite was
added to reject. The repo's layout is not evidence about the container's.

A repo-root build context is not the right signal either: coding_env builds
from the repo root but `pip install ./envs/coding_env/` makes it a real
distribution, so its CMD launches `coding_env.server.app:app` with no prefix.
The prefix is only importable when the image launches it that way, as
grid_world_env does with /app on PYTHONPATH. Derive it from that.

Add the negative tests whose absence let this through: the checks only proved
the values in the tree pass, never that a wrong one fails. One asserts AWM's
old target does not resolve, one that grid_world's legitimate target still
does, and one pins repo-root-context and envs-prefix as different predicates.
@k21993

k21993 commented Aug 6, 2026

Copy link
Copy Markdown
Author

@cursor Right again, and this one was worse than its Medium rating and the suite was passing the exact bug the PR exists to fix. Verified before fixing by putting the original value back:

app: envs.agent_world_model_env.server.app:app     # the original bug
71 passed

Fixed in 0b3e94c. The repo-root fallback is now conditional on evidence that the image actually imports under that prefix, rather than on the path existing in the source tree, which it always does.

One refinement worth noting: "built from a repo-root context" is not the right predicate. coding_env is built that way, but pip install ./envs/coding_env/ turns it into a real distribution, so its CMD is coding_env.server.app:app with no prefix. The only thing that evidences the prefix is the image launching it that way, as grid_world_env does (/app/envs/grid_world_env with /app on PYTHONPATH). The helper keys off that.

Same value with the bad manifest restored, after the fix:

FAILED test_manifest_app_target_exists[agent_world_model_env]
  module 'envs.agent_world_model_env.server.app' does not exist under
  envs/agent_world_model_env

Also added the negative tests whose absence let this through. Every check here only proved that the values in the tree pass — nothing asserted that a wrong value fails, which is why the regression was invisible to it:

  • test_rejects_envs_prefix_for_env_root_layout — AWM's old target must not resolve
  • test_accepts_envs_prefix_for_repo_root_layout — grid_world's legitimate target still must
  • test_repo_root_build_context_alone_does_not_allow_the_prefix — pins the two predicates apart

Suite: 1604 passed, 133 skipped.

@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

Unable to authenticate your request. Please make sure to connect your GitHub account to Cursor. Go to Cursor

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.

1 participant