openenv 0.4.2__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. {openenv-0.4.2/src/openenv.egg-info → openenv-0.5.0}/PKG-INFO +6 -3
  2. {openenv-0.4.2 → openenv-0.5.0}/README.md +2 -2
  3. {openenv-0.4.2 → openenv-0.5.0}/pyproject.toml +16 -1
  4. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/auto/_discovery.py +159 -9
  5. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/__main__.py +12 -3
  6. openenv-0.5.0/src/openenv/cli/commands/catalog.py +121 -0
  7. openenv-0.5.0/src/openenv/cli/commands/harbor.py +715 -0
  8. openenv-0.5.0/src/openenv/cli/commands/validate.py +247 -0
  9. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/templates/openenv_env/README.md +8 -2
  10. openenv-0.5.0/src/openenv/cli/templates/openenv_env/openenv.yaml +23 -0
  11. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/templates/openenv_env/server/__ENV_NAME___environment.py +2 -2
  12. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_client.py +181 -20
  13. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/http_server.py +99 -16
  14. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/serialization.py +13 -3
  15. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/web_interface.py +4 -0
  16. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/harness/__init__.py +101 -1
  17. openenv-0.5.0/src/openenv/core/harness/capture/__init__.py +37 -0
  18. openenv-0.5.0/src/openenv/core/harness/capture/compat.py +287 -0
  19. openenv-0.5.0/src/openenv/core/harness/capture/contract.py +331 -0
  20. openenv-0.5.0/src/openenv/core/harness/capture/detection.py +65 -0
  21. openenv-0.5.0/src/openenv/core/harness/capture/dialects/__init__.py +23 -0
  22. openenv-0.5.0/src/openenv/core/harness/capture/dialects/anthropic.py +705 -0
  23. openenv-0.5.0/src/openenv/core/harness/capture/dialects/base.py +134 -0
  24. openenv-0.5.0/src/openenv/core/harness/capture/dialects/google.py +729 -0
  25. openenv-0.5.0/src/openenv/core/harness/capture/dialects/images.py +335 -0
  26. openenv-0.5.0/src/openenv/core/harness/capture/dialects/openai_chat.py +41 -0
  27. openenv-0.5.0/src/openenv/core/harness/capture/dialects/openai_responses.py +1125 -0
  28. openenv-0.5.0/src/openenv/core/harness/capture/dialects/reasoning.py +125 -0
  29. openenv-0.5.0/src/openenv/core/harness/capture/export.py +246 -0
  30. openenv-0.5.0/src/openenv/core/harness/capture/forwarding.py +346 -0
  31. openenv-0.5.0/src/openenv/core/harness/capture/graph.py +496 -0
  32. openenv-0.5.0/src/openenv/core/harness/capture/providers.py +325 -0
  33. openenv-0.5.0/src/openenv/core/harness/capture/runner.py +179 -0
  34. openenv-0.5.0/src/openenv/core/harness/capture/server.py +1474 -0
  35. openenv-0.5.0/src/openenv/core/harness/capture/sessions.py +280 -0
  36. openenv-0.5.0/src/openenv/core/harness/capture/sse.py +255 -0
  37. openenv-0.5.0/src/openenv/core/harness/capture/upstream.py +603 -0
  38. openenv-0.5.0/src/openenv/core/harness/capture/validate.py +455 -0
  39. openenv-0.5.0/src/openenv/core/harness/capture/validate_llm.py +768 -0
  40. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/harness/collect.py +57 -14
  41. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/mcp_client.py +93 -27
  42. openenv-0.5.0/src/openenv/discovery/__init__.py +34 -0
  43. openenv-0.5.0/src/openenv/discovery/errors.py +5 -0
  44. openenv-0.5.0/src/openenv/discovery/metadata.py +191 -0
  45. openenv-0.5.0/src/openenv/discovery/models.py +425 -0
  46. openenv-0.5.0/src/openenv/discovery/producer.py +309 -0
  47. openenv-0.5.0/src/openenv/discovery/repository.py +148 -0
  48. openenv-0.5.0/src/openenv/discovery/schemas/0.1-draft/catalog.schema.json +715 -0
  49. openenv-0.5.0/src/openenv/discovery/schemas/0.1-draft/declaration.schema.json +171 -0
  50. openenv-0.5.0/src/openenv/discovery/schemas/0.1-draft/environment-card.schema.json +396 -0
  51. openenv-0.5.0/src/openenv/discovery/search.py +131 -0
  52. openenv-0.5.0/src/openenv/discovery/serialization.py +127 -0
  53. openenv-0.5.0/src/openenv/harbor/__init__.py +18 -0
  54. openenv-0.5.0/src/openenv/harbor/atif.py +563 -0
  55. openenv-0.5.0/src/openenv/harbor/capabilities.py +316 -0
  56. openenv-0.5.0/src/openenv/harbor/client.py +216 -0
  57. openenv-0.5.0/src/openenv/harbor/contract.py +193 -0
  58. openenv-0.5.0/src/openenv/harbor/e2b_stream.py +64 -0
  59. openenv-0.5.0/src/openenv/harbor/environment.py +338 -0
  60. openenv-0.5.0/src/openenv/harbor/install_fixes.py +698 -0
  61. openenv-0.5.0/src/openenv/harbor/models.py +352 -0
  62. openenv-0.5.0/src/openenv/harbor/nemo_profile.py +26 -0
  63. openenv-0.5.0/src/openenv/harbor/proc_env_context.py +133 -0
  64. openenv-0.5.0/src/openenv/harbor/qualification.py +154 -0
  65. openenv-0.5.0/src/openenv/harbor/rollout.py +777 -0
  66. openenv-0.5.0/src/openenv/harbor/runner.py +228 -0
  67. openenv-0.5.0/src/openenv/harbor/seams.py +966 -0
  68. openenv-0.5.0/src/openenv/harbor/serving.py +324 -0
  69. openenv-0.5.0/src/openenv/harbor/shared_template.py +106 -0
  70. openenv-0.5.0/src/openenv/harbor/startup.py +212 -0
  71. openenv-0.5.0/src/openenv/harbor/tasks.py +302 -0
  72. openenv-0.5.0/src/openenv/harbor/ui.py +1511 -0
  73. openenv-0.5.0/src/openenv/validation/__init__.py +94 -0
  74. openenv-0.5.0/src/openenv/validation/graders/__init__.py +165 -0
  75. openenv-0.5.0/src/openenv/validation/graders/static/__init__.py +5 -0
  76. openenv-0.5.0/src/openenv/validation/graders/static/manifest.py +72 -0
  77. openenv-0.5.0/src/openenv/validation/manifest.py +306 -0
  78. openenv-0.5.0/src/openenv/validation/parsers/__init__.py +69 -0
  79. openenv-0.5.0/src/openenv/validation/parsers/openenv_yaml.py +96 -0
  80. openenv-0.5.0/src/openenv/validation/policies/severity-v1.json +56 -0
  81. openenv-0.5.0/src/openenv/validation/policy.py +149 -0
  82. openenv-0.5.0/src/openenv/validation/providers/__init__.py +64 -0
  83. openenv-0.5.0/src/openenv/validation/report.py +90 -0
  84. openenv-0.5.0/src/openenv/validation/runner.py +150 -0
  85. openenv-0.5.0/src/openenv/validation/schemas/manifest.schema.json +430 -0
  86. openenv-0.5.0/src/openenv/validation/schemas/report.schema.json +591 -0
  87. openenv-0.5.0/src/openenv/validation/signature.py +120 -0
  88. openenv-0.5.0/src/openenv/validation/types.py +88 -0
  89. {openenv-0.4.2 → openenv-0.5.0/src/openenv.egg-info}/PKG-INFO +6 -3
  90. {openenv-0.4.2 → openenv-0.5.0}/src/openenv.egg-info/SOURCES.txt +72 -0
  91. {openenv-0.4.2 → openenv-0.5.0}/src/openenv.egg-info/requires.txt +6 -0
  92. openenv-0.4.2/src/openenv/cli/commands/validate.py +0 -197
  93. openenv-0.4.2/src/openenv/cli/templates/openenv_env/openenv.yaml +0 -7
  94. {openenv-0.4.2 → openenv-0.5.0}/LICENSE +0 -0
  95. {openenv-0.4.2 → openenv-0.5.0}/MANIFEST.in +0 -0
  96. {openenv-0.4.2 → openenv-0.5.0}/setup.cfg +0 -0
  97. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/__init__.py +0 -0
  98. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/auto/__init__.py +0 -0
  99. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/auto/auto_action.py +0 -0
  100. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/auto/auto_env.py +0 -0
  101. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/__init__.py +0 -0
  102. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/_cli_utils.py +0 -0
  103. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/_validation.py +0 -0
  104. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/commands/__init__.py +0 -0
  105. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/commands/build.py +0 -0
  106. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/commands/collect.py +0 -0
  107. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/commands/fork.py +0 -0
  108. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/commands/import_env.py +0 -0
  109. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/commands/init.py +0 -0
  110. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/commands/push.py +0 -0
  111. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/commands/serve.py +0 -0
  112. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/commands/skills.py +0 -0
  113. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/importers/__init__.py +0 -0
  114. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/importers/base.py +0 -0
  115. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/importers/ors.py +0 -0
  116. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/importers/templates/__init__.py +0 -0
  117. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/importers/templates/ors_app.py.tpl +0 -0
  118. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/importers/templates/ors_environment.py.tpl +0 -0
  119. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/importers/templates/verifiers_app.py.tpl +0 -0
  120. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/importers/templates/verifiers_environment.py.tpl +0 -0
  121. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/importers/verifiers.py +0 -0
  122. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/templates/__init__.py +0 -0
  123. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/templates/openenv_env/__init__.py +0 -0
  124. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/templates/openenv_env/client.py +0 -0
  125. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/templates/openenv_env/models.py +0 -0
  126. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/templates/openenv_env/pyproject.toml +0 -0
  127. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/templates/openenv_env/server/Dockerfile +0 -0
  128. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/templates/openenv_env/server/__init__.py +0 -0
  129. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/templates/openenv_env/server/app.py +0 -0
  130. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/templates/openenv_env/server/requirements.txt +0 -0
  131. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/__init__.py +0 -0
  132. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/client_types.py +0 -0
  133. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/containers/__init__.py +0 -0
  134. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/containers/runtime/__init__.py +0 -0
  135. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/containers/runtime/_server_config.py +0 -0
  136. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/containers/runtime/aca_provider.py +0 -0
  137. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/containers/runtime/daytona_provider.py +0 -0
  138. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/containers/runtime/hf_sandbox_provider.py +0 -0
  139. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/containers/runtime/modal_provider.py +0 -0
  140. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/containers/runtime/providers.py +0 -0
  141. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/containers/runtime/uv_provider.py +0 -0
  142. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/containers/test_local_docker_provider.py +0 -0
  143. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/__init__.py +0 -0
  144. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/_utils.py +0 -0
  145. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/base_transforms.py +0 -0
  146. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/exceptions.py +0 -0
  147. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/gradio_theme.py +0 -0
  148. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/gradio_ui.py +0 -0
  149. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/interfaces.py +0 -0
  150. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/mcp_environment.py +0 -0
  151. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/mcp_types.py +0 -0
  152. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/route_config.py +0 -0
  153. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/types.py +0 -0
  154. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/evals/__init__.py +0 -0
  155. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/evals/base.py +0 -0
  156. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/evals/inspect_harness.py +0 -0
  157. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/evals/types.py +0 -0
  158. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/generic_client.py +0 -0
  159. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/llm_client.py +0 -0
  160. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/rubrics/__init__.py +0 -0
  161. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/rubrics/base.py +0 -0
  162. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/rubrics/containers.py +0 -0
  163. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/rubrics/llm_judge.py +0 -0
  164. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/rubrics/trajectory.py +0 -0
  165. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/sync_client.py +0 -0
  166. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/tools/__init__.py +0 -0
  167. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/tools/git_server_client.py +0 -0
  168. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/tools/local_python_executor.py +0 -0
  169. {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/utils.py +0 -0
  170. {openenv-0.4.2 → openenv-0.5.0}/src/openenv.egg-info/dependency_links.txt +0 -0
  171. {openenv-0.4.2 → openenv-0.5.0}/src/openenv.egg-info/entry_points.txt +0 -0
  172. {openenv-0.4.2 → openenv-0.5.0}/src/openenv.egg-info/top_level.txt +0 -0
  173. {openenv-0.4.2 → openenv-0.5.0}/src/openenv_core/__init__.py +0 -0
  174. {openenv-0.4.2 → openenv-0.5.0}/tests/test_line_endings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openenv
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: A unified framework for reinforcement learning environments
5
5
  License-Expression: BSD-3-Clause
6
6
  Requires-Python: >=3.10
@@ -8,6 +8,7 @@ Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
9
  Requires-Dist: fastapi>=0.116.0
10
10
  Requires-Dist: pydantic>=2.0.0
11
+ Requires-Dist: packaging>=24.2
11
12
  Requires-Dist: typing_extensions>=4.0.0
12
13
  Requires-Dist: uvicorn>=0.24.0
13
14
  Requires-Dist: requests>=2.25.0
@@ -33,6 +34,8 @@ Requires-Dist: modal>=1.4.0; extra == "modal"
33
34
  Requires-Dist: pyyaml>=6.0; extra == "modal"
34
35
  Provides-Extra: inspect
35
36
  Requires-Dist: inspect-ai>=0.3.0; extra == "inspect"
37
+ Provides-Extra: harbor
38
+ Requires-Dist: harbor[beam,blaxel,cua,cwsandbox,daytona,e2b,ec2,gke,islo,modal,novita,opensandbox,runloop,use-computer]>=0.22.0; python_version >= "3.12" and extra == "harbor"
36
39
  Dynamic: license-file
37
40
 
38
41
  # OpenEnv: Agentic Execution Environments
@@ -409,11 +412,11 @@ See the [Oumi example](https://github.com/oumi-ai/oumi/blob/main/notebooks/Oumi%
409
412
 
410
413
  ## Community Support & Acknowledgments
411
414
 
412
- OpenEnv is governed by a technical committee that coordinates project direction, major technical decisions, RFCs, and release planning through the public issue tracker, pull requests, and RFC process. Current committee members: Meta-PyTorch, Reflection, Unsloth, Modal, Prime Intellect, Nvidia, Mercor, Fleet AI, Microsoft, Hugging Face, and RadixArk.
415
+ OpenEnv is governed by a technical committee that coordinates project direction, major technical decisions, RFCs, and release planning through the public issue tracker, pull requests, and RFC process. Current committee members: Meta-PyTorch, Reflection, Unsloth, Modal, Prime Intellect, Nvidia, Mercor, Fleet AI, Microsoft, Hugging Face, RadixArk, and Nebius.
413
416
 
414
417
  The project is also supported by a broader community of organizations. If you would like to add your project or organization here, please open a pull request for maintainer review.
415
418
 
416
- Supporters include: [Meta-PyTorch](https://github.com/meta-pytorch), [Hugging Face](https://huggingface.co), [Scaler AI Labs](https://scalerailabs.com), [Patronus AI](https://patronus.ai), [Surge AI](https://surgehq.ai), [LastMile AI](https://www.lastmileai.dev), [Unsloth](https://unsloth.ai), [Reflection](https://reflection.ai), [vLLM](https://vllm.ai), [SkyRL](https://skyrl.readthedocs.io) (UC-Berkeley), [Lightning AI](https://lightning.ai), [Axolotl AI](https://github.com/axolotl-ai-cloud/axolotl), [Stanford Scaling Intelligence Lab](https://scalingintelligence.stanford.edu/), [Mithril](https://mithril.ai), [OpenMined](https://openmined.org/), [Fleet AI](https://fleetai.com), [Halluminate](https://halluminate.ai/), [Turing](https://www.turing.com/), [Scale AI](https://scale.com/), [Scorecard](https://www.scorecard.io/), [Snorkel AI](https://snorkel.ai/), [SGLang](https://github.com/sgl-project/sglang), [Miles](https://github.com/radixark/miles)
419
+ Supporters include: [Meta-PyTorch](https://github.com/meta-pytorch), [Hugging Face](https://huggingface.co), [Scaler AI Labs](https://scalerailabs.com), [Patronus AI](https://patronus.ai), [Surge AI](https://surgehq.ai), [LastMile AI](https://www.lastmileai.dev), [Unsloth](https://unsloth.ai), [Reflection](https://reflection.ai), [vLLM](https://vllm.ai), [SkyRL](https://skyrl.readthedocs.io) (UC-Berkeley), [Lightning AI](https://lightning.ai), [Axolotl AI](https://github.com/axolotl-ai-cloud/axolotl), [Stanford Scaling Intelligence Lab](https://scalingintelligence.stanford.edu/), [Mithril](https://mithril.ai), [OpenMined](https://openmined.org/), [Fleet AI](https://fleetai.com), [Halluminate](https://halluminate.ai/), [Turing](https://www.turing.com/), [Scale AI](https://scale.com/), [Scorecard](https://www.scorecard.io/), [Snorkel AI](https://snorkel.ai/), [SGLang](https://github.com/sgl-project/sglang), [Miles](https://github.com/radixark/miles), [Nebius](https://nebius.com)
417
420
 
418
421
  And we'd also like to acknowledge the team at Farama Foundation as the OpenEnv API was heavily inspired by the work you all have done on Gymnasium. Cheers!
419
422
 
@@ -372,11 +372,11 @@ See the [Oumi example](https://github.com/oumi-ai/oumi/blob/main/notebooks/Oumi%
372
372
 
373
373
  ## Community Support & Acknowledgments
374
374
 
375
- OpenEnv is governed by a technical committee that coordinates project direction, major technical decisions, RFCs, and release planning through the public issue tracker, pull requests, and RFC process. Current committee members: Meta-PyTorch, Reflection, Unsloth, Modal, Prime Intellect, Nvidia, Mercor, Fleet AI, Microsoft, Hugging Face, and RadixArk.
375
+ OpenEnv is governed by a technical committee that coordinates project direction, major technical decisions, RFCs, and release planning through the public issue tracker, pull requests, and RFC process. Current committee members: Meta-PyTorch, Reflection, Unsloth, Modal, Prime Intellect, Nvidia, Mercor, Fleet AI, Microsoft, Hugging Face, RadixArk, and Nebius.
376
376
 
377
377
  The project is also supported by a broader community of organizations. If you would like to add your project or organization here, please open a pull request for maintainer review.
378
378
 
379
- Supporters include: [Meta-PyTorch](https://github.com/meta-pytorch), [Hugging Face](https://huggingface.co), [Scaler AI Labs](https://scalerailabs.com), [Patronus AI](https://patronus.ai), [Surge AI](https://surgehq.ai), [LastMile AI](https://www.lastmileai.dev), [Unsloth](https://unsloth.ai), [Reflection](https://reflection.ai), [vLLM](https://vllm.ai), [SkyRL](https://skyrl.readthedocs.io) (UC-Berkeley), [Lightning AI](https://lightning.ai), [Axolotl AI](https://github.com/axolotl-ai-cloud/axolotl), [Stanford Scaling Intelligence Lab](https://scalingintelligence.stanford.edu/), [Mithril](https://mithril.ai), [OpenMined](https://openmined.org/), [Fleet AI](https://fleetai.com), [Halluminate](https://halluminate.ai/), [Turing](https://www.turing.com/), [Scale AI](https://scale.com/), [Scorecard](https://www.scorecard.io/), [Snorkel AI](https://snorkel.ai/), [SGLang](https://github.com/sgl-project/sglang), [Miles](https://github.com/radixark/miles)
379
+ Supporters include: [Meta-PyTorch](https://github.com/meta-pytorch), [Hugging Face](https://huggingface.co), [Scaler AI Labs](https://scalerailabs.com), [Patronus AI](https://patronus.ai), [Surge AI](https://surgehq.ai), [LastMile AI](https://www.lastmileai.dev), [Unsloth](https://unsloth.ai), [Reflection](https://reflection.ai), [vLLM](https://vllm.ai), [SkyRL](https://skyrl.readthedocs.io) (UC-Berkeley), [Lightning AI](https://lightning.ai), [Axolotl AI](https://github.com/axolotl-ai-cloud/axolotl), [Stanford Scaling Intelligence Lab](https://scalingintelligence.stanford.edu/), [Mithril](https://mithril.ai), [OpenMined](https://openmined.org/), [Fleet AI](https://fleetai.com), [Halluminate](https://halluminate.ai/), [Turing](https://www.turing.com/), [Scale AI](https://scale.com/), [Scorecard](https://www.scorecard.io/), [Snorkel AI](https://snorkel.ai/), [SGLang](https://github.com/sgl-project/sglang), [Miles](https://github.com/radixark/miles), [Nebius](https://nebius.com)
380
380
 
381
381
  And we'd also like to acknowledge the team at Farama Foundation as the OpenEnv API was heavily inspired by the work you all have done on Gymnasium. Cheers!
382
382
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "openenv"
7
- version = "0.4.2"
7
+ version = "0.5.0"
8
8
  description = "A unified framework for reinforcement learning environments"
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
@@ -16,6 +16,7 @@ dependencies = [
16
16
  # individual environment pyproject.toml files
17
17
  "fastapi>=0.116.0",
18
18
  "pydantic>=2.0.0",
19
+ "packaging>=24.2",
19
20
  "typing_extensions>=4.0.0",
20
21
  "uvicorn>=0.24.0",
21
22
  "requests>=2.25.0",
@@ -55,6 +56,17 @@ modal = [
55
56
  inspect = [
56
57
  "inspect-ai>=0.3.0",
57
58
  ]
59
+ harbor = [
60
+ # Every sandbox backend. Bare `harbor` gives one that lists all 23 backends and can
61
+ # instantiate none of them: each raises MissingExtraError from its constructor, which
62
+ # surfaces as a failed rollout rather than as a missing dependency.
63
+ #
64
+ # Not `harbor[cloud]`, which is unsatisfiable: it pulls `langsmith[sandbox]`
65
+ # (websockets>=15) and `tensorlake` (websockets>=13,<14) together.
66
+ #
67
+ # Harbor requires Python >= 3.12, so this extra is excluded from the 3.11 CI leg.
68
+ "harbor[e2b,modal,daytona,gke,ec2,runloop,novita,blaxel,beam,islo,opensandbox,cwsandbox,use-computer,cua]>=0.22.0; python_version >= '3.12'",
69
+ ]
58
70
 
59
71
  [project.scripts]
60
72
  openenv = "openenv.cli.__main__:main"
@@ -66,6 +78,8 @@ include-package-data = true
66
78
  [tool.setuptools.package-data]
67
79
  "openenv.cli" = ["templates/**/*"]
68
80
  "openenv.cli.importers" = ["templates/**/*"]
81
+ "openenv.validation" = ["policies/*.json", "schemas/*.json"]
82
+ "openenv.discovery" = ["schemas/**/*.json"]
69
83
 
70
84
  [tool.setuptools.exclude-package-data]
71
85
  "*" = ["*.pyc", "*.pyo", "__pycache__/*"]
@@ -106,6 +120,7 @@ dev = [
106
120
  "usort>=1.1.0",
107
121
  "pytest>=7.0",
108
122
  "pytest-asyncio>=0.21",
123
+ "jsonschema>=4.20.0",
109
124
  ]
110
125
 
111
126
  [tool.usort]
@@ -20,8 +20,9 @@ import importlib.metadata
20
20
  import importlib.resources
21
21
  import json
22
22
  import logging
23
+ import os
23
24
  import re
24
- import tempfile
25
+ import stat
25
26
  from dataclasses import asdict, dataclass
26
27
  from pathlib import Path
27
28
  from typing import Any, Type
@@ -336,6 +337,118 @@ def _create_env_info_from_package(
336
337
  )
337
338
 
338
339
 
340
+ def _default_cache_file() -> Path:
341
+ """
342
+ Return the per-user discovery cache file path.
343
+
344
+ Uses a per-user cache directory (``$XDG_CACHE_HOME`` or ``~/.cache``) rather than a
345
+ shared, world-writable temporary directory. A fixed path under the shared temp dir
346
+ lets another local user pre-create the cache file and redirect discovery to
347
+ attacker-controlled import paths (`import_module` on a cached `client_module_path`).
348
+ Per the XDG Base Directory specification, relative `XDG_CACHE_HOME` values are
349
+ ignored so an untrusted working tree cannot supply a victim-owned cache file.
350
+ The fallback home must itself be absolute; otherwise discovery fails closed.
351
+ """
352
+ base = os.environ.get("XDG_CACHE_HOME")
353
+ if base and Path(base).is_absolute():
354
+ root = Path(base)
355
+ else:
356
+ home = Path.home()
357
+ if not home.is_absolute():
358
+ raise RuntimeError(
359
+ "discovery cache requires an absolute home directory when "
360
+ "XDG_CACHE_HOME is unset or relative"
361
+ )
362
+ root = home / ".cache"
363
+ return root / "openenv" / "discovery_cache.json"
364
+
365
+
366
+ def _is_trusted_stat(info: os.stat_result) -> bool:
367
+ """
368
+ Return whether the file *info* describes is safe to load.
369
+
370
+ On POSIX the file must be owned by the current user and not writable by group or
371
+ others, so a cache file planted by another user is ignored rather than trusted.
372
+
373
+ Args:
374
+ info (`os.stat_result`):
375
+ Metadata for the file being considered.
376
+
377
+ Returns:
378
+ `bool`: `True` if the file is owned by the current user and not
379
+ group/other-writable (always `True` on non-POSIX platforms).
380
+ """
381
+ if os.name != "posix":
382
+ return True
383
+ return info.st_uid == os.getuid() and not (
384
+ info.st_mode & (stat.S_IWGRP | stat.S_IWOTH)
385
+ )
386
+
387
+
388
+ def _is_trusted_cache_file(path: Path) -> bool:
389
+ """
390
+ Return whether *path* is safe to load.
391
+
392
+ Prefer `_open_trusted_cache`, which checks the descriptor it hands back. This
393
+ variant resolves the path a second time, so on its own it cannot promise that
394
+ the file inspected is the file later read.
395
+
396
+ Args:
397
+ path (`Path`):
398
+ The cache file to check.
399
+
400
+ Returns:
401
+ `bool`: `True` if the file is owned by the current user and not
402
+ group/other-writable (always `True` on non-POSIX platforms).
403
+ """
404
+ if os.name != "posix":
405
+ return True
406
+ try:
407
+ return _is_trusted_stat(path.stat())
408
+ except OSError:
409
+ return False
410
+
411
+
412
+ def _open_trusted_cache(path: Path) -> int | None:
413
+ """
414
+ Open *path* for reading and return the descriptor only if it is trustworthy.
415
+
416
+ Checking a path and then opening it are two separate resolutions, and an
417
+ attacker who can write in the cache directory can swap the file for a symlink
418
+ in between, so the file that was vetted is not the file that gets read.
419
+ ``O_NOFOLLOW`` refuses a symlink outright and ``fstat`` inspects the descriptor
420
+ itself, which makes the ownership check and the read the same object.
421
+
422
+ Args:
423
+ path (`Path`):
424
+ The cache file to open.
425
+
426
+ Returns:
427
+ `int` or `None`: an open read-only descriptor the caller must close, or
428
+ `None` if the file is missing, is a symlink, is not a regular file, or
429
+ is not owned by the current user.
430
+ """
431
+ # O_NONBLOCK matters before the descriptor can be inspected: opening a FIFO
432
+ # read-only otherwise waits for a writer that an attacker simply never
433
+ # provides, so discovery hangs before any check runs. It has no effect on a
434
+ # regular file, which is the only thing accepted here anyway.
435
+ flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)
436
+ try:
437
+ fd = os.open(path, flags)
438
+ except OSError:
439
+ # Missing, unreadable, or a symlink (ELOOP under O_NOFOLLOW).
440
+ return None
441
+ try:
442
+ info = os.fstat(fd)
443
+ if not stat.S_ISREG(info.st_mode) or not _is_trusted_stat(info):
444
+ os.close(fd)
445
+ return None
446
+ except OSError:
447
+ os.close(fd)
448
+ return None
449
+ return fd
450
+
451
+
339
452
  class EnvironmentDiscovery:
340
453
  """
341
454
  Auto-discovery system for OpenEnv environments using installed packages.
@@ -346,7 +459,7 @@ class EnvironmentDiscovery:
346
459
  def __init__(self):
347
460
  """Initialize discovery system."""
348
461
  self._cache: dict[str, EnvironmentInfo] | None = None
349
- self._cache_file = Path(tempfile.gettempdir()) / "openenv_discovery_cache.json"
462
+ self._cache_file = _default_cache_file()
350
463
 
351
464
  def _discover_installed_packages(self) -> dict[str, EnvironmentInfo]:
352
465
  """
@@ -408,11 +521,22 @@ class EnvironmentDiscovery:
408
521
  Returns:
409
522
  Dictionary of env_key -> EnvironmentInfo, or None if cache invalid
410
523
  """
411
- if not self._cache_file.exists():
524
+ # Only trust a cache file owned by the current user. This prevents another
525
+ # local user from planting a file that would redirect discovery (and the
526
+ # subsequent import_module) to attacker-controlled modules/classes. The
527
+ # descriptor is what gets vetted and then read, so the file cannot be
528
+ # swapped for a symlink after the check.
529
+ fd = _open_trusted_cache(self._cache_file)
530
+ if fd is None:
531
+ if self._cache_file.exists():
532
+ logger.warning(
533
+ f"Ignoring discovery cache {self._cache_file}: not owned by "
534
+ "the current user, writable by group/others, or a symlink."
535
+ )
412
536
  return None
413
537
 
414
538
  try:
415
- with open(self._cache_file, "r") as f:
539
+ with os.fdopen(fd, "r") as f:
416
540
  cache_data = json.load(f)
417
541
 
418
542
  # Reconstruct EnvironmentInfo objects
@@ -437,8 +561,30 @@ class EnvironmentDiscovery:
437
561
  for env_key, env_info in environments.items():
438
562
  cache_data[env_key] = asdict(env_info)
439
563
 
440
- with open(self._cache_file, "w") as f:
441
- json.dump(cache_data, f, indent=2)
564
+ self._cache_file.parent.mkdir(parents=True, exist_ok=True)
565
+ # Write a new file and move it into place rather than writing
566
+ # through whatever already sits at the destination. A mode passed to
567
+ # `os.open` only applies when it creates the file, so truncating an
568
+ # existing group/world-writable cache would keep that mode and
569
+ # publish what was just written; and `os.replace` is atomic, so a
570
+ # reader never observes a half-written cache when serialization
571
+ # fails part way. `O_EXCL` requires the temporary name to be new,
572
+ # and 0600 is applied at creation so the umask cannot widen it.
573
+ tmp_path = self._cache_file.with_name(
574
+ f"{self._cache_file.name}.{os.getpid()}.tmp"
575
+ )
576
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0)
577
+ fd = os.open(tmp_path, flags, 0o600)
578
+ try:
579
+ with os.fdopen(fd, "w") as f:
580
+ json.dump(cache_data, f, indent=2)
581
+ os.replace(tmp_path, self._cache_file)
582
+ except BaseException:
583
+ try:
584
+ os.unlink(tmp_path)
585
+ except OSError:
586
+ pass
587
+ raise
442
588
 
443
589
  except Exception as e:
444
590
  logger.warning(f"Failed to save discovery cache: {e}")
@@ -575,8 +721,12 @@ def get_discovery() -> EnvironmentDiscovery:
575
721
 
576
722
 
577
723
  def reset_discovery() -> None:
578
- """Reset the global discovery instance (useful for testing)."""
724
+ """Drop the global discovery singleton (useful for testing).
725
+
726
+ Does **not** delete the on-disk cache. The cache is now a persistent
727
+ per-user file under `$XDG_CACHE_HOME`; wiping it belongs on an explicit
728
+ [`EnvironmentDiscovery.clear_cache`][] call so test fixtures that only
729
+ need a fresh singleton cannot clobber a developer's real cache.
730
+ """
579
731
  global _global_discovery
580
- if _global_discovery is not None:
581
- _global_discovery.clear_cache()
582
732
  _global_discovery = None
@@ -12,8 +12,10 @@ import sys
12
12
  import typer
13
13
  from openenv.cli.commands import (
14
14
  build,
15
+ catalog,
15
16
  collect,
16
17
  fork,
18
+ harbor,
17
19
  import_env,
18
20
  init,
19
21
  push,
@@ -37,9 +39,14 @@ app.command(name="import", help="Import a third-party environment into OpenEnv")
37
39
  app.command(name="build", help="Build Docker images for OpenEnv environments")(
38
40
  build.build
39
41
  )
40
- app.command(
41
- name="validate", help="Validate environment structure and deployment readiness"
42
- )(validate.validate)
42
+ app.add_typer(
43
+ harbor.app,
44
+ name="harbor",
45
+ help="Run Harbor tasks with token-level capture (requires: pip install openenv[harbor])",
46
+ )
47
+ app.command(name="validate", help="Validate a package against the OpenEnv quality bar")(
48
+ validate.validate
49
+ )
43
50
  app.command(
44
51
  name="push",
45
52
  help="Push an OpenEnv environment to Hugging Face Spaces or custom registry",
@@ -60,6 +67,8 @@ app.command(
60
67
  name="collect",
61
68
  help="Collect rollouts from a deployed OpenEnv environment",
62
69
  )(collect.collect)
70
+ app.add_typer(catalog.app, name="catalog")
71
+ app.command(name="discover")(catalog.discover)
63
72
 
64
73
 
65
74
  # Entry point for setuptools
@@ -0,0 +1,121 @@
1
+ # SPDX-License-Identifier: BSD-3-Clause
2
+
3
+ """Metadata catalog production and inspection. No environment is instantiated."""
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import typer
9
+ from openenv.discovery import (
10
+ build_catalog,
11
+ CatalogError,
12
+ load_catalog,
13
+ resolve_entry,
14
+ search_catalog,
15
+ write_catalog,
16
+ )
17
+
18
+ app = typer.Typer(
19
+ help="Build and inspect versioned, metadata-only environment catalogs."
20
+ )
21
+
22
+
23
+ @app.command("build")
24
+ def build(
25
+ repository: Path = typer.Option(..., help="Local Git repository to read."),
26
+ repository_uri: str = typer.Option(..., help="Declared GitHub HTTPS clone URI."),
27
+ publisher: str = typer.Option(..., help="Explicit catalog publication authority."),
28
+ output: Path = typer.Option(..., help="Destination snapshot JSON."),
29
+ revision: str = typer.Option("HEAD", help="Source commit or ref."),
30
+ root: str = typer.Option("envs", help="Direct-child environment inventory root."),
31
+ namespace: str = typer.Option("openenv", help="Publisher-owned namespace."),
32
+ ) -> None:
33
+ """Build from committed metadata; uncommitted files and candidate code are not used."""
34
+ try:
35
+ snapshot = build_catalog(
36
+ repository,
37
+ repository_uri=repository_uri,
38
+ publisher=publisher,
39
+ namespace=namespace,
40
+ revision=revision,
41
+ root=root,
42
+ )
43
+ write_catalog(snapshot, output)
44
+ except CatalogError as error:
45
+ typer.echo(f"Catalog error: {error}", err=True)
46
+ raise typer.Exit(1) from error
47
+ typer.echo(
48
+ f"{len(snapshot.entries)} records, snapshot {snapshot.digest}, "
49
+ f"complete={str(snapshot.complete).lower()}",
50
+ err=True,
51
+ )
52
+ for issue in snapshot.issues:
53
+ typer.echo(f"{issue.severity}: {issue.path}: {issue.message}", err=True)
54
+ if not snapshot.complete:
55
+ raise typer.Exit(1)
56
+
57
+
58
+ @app.command("inspect")
59
+ def inspect(
60
+ identifier: str = typer.Argument(..., help="Exact revision-card identifier."),
61
+ catalog: Path = typer.Option(..., help="Complete configured snapshot."),
62
+ ) -> None:
63
+ """Return the complete selected entry without guessing or fetching a URL."""
64
+ try:
65
+ entry = resolve_entry(load_catalog(catalog), identifier)
66
+ except CatalogError as error:
67
+ typer.echo(f"Catalog error: {error}", err=True)
68
+ raise typer.Exit(1) from error
69
+ typer.echo(entry.model_dump_json(by_alias=True, exclude_none=True, indent=2))
70
+
71
+
72
+ def discover(
73
+ query: str = typer.Argument("", help="Task to find an environment for."),
74
+ catalog: Path = typer.Option(
75
+ ..., help="Complete, explicitly configured local snapshot."
76
+ ),
77
+ limit: int = typer.Option(20, min=1, max=1000),
78
+ filter: list[str] = typer.Option([], "--filter", help="Exact field=value filter."),
79
+ as_json: bool = typer.Option(False, "--json", help="Emit complete ARD entry data."),
80
+ ) -> None:
81
+ """Find unfamiliar environments by task before any installation or execution."""
82
+ filters = {}
83
+ for item in filter:
84
+ key, separator, value = item.partition("=")
85
+ if not separator or not key or key in filters:
86
+ typer.echo(
87
+ "Catalog error: filters must be distinct field=value pairs", err=True
88
+ )
89
+ raise typer.Exit(1)
90
+ filters[key] = value
91
+ try:
92
+ snapshot = load_catalog(catalog)
93
+ matches = search_catalog(snapshot, query, filters=filters, limit=limit)
94
+ except CatalogError as error:
95
+ typer.echo(f"Catalog error: {error}", err=True)
96
+ raise typer.Exit(1) from error
97
+ if as_json:
98
+ results = [
99
+ {
100
+ **match.entry.model_dump(by_alias=True, exclude_none=True, mode="json"),
101
+ "score": match.score,
102
+ }
103
+ for match in matches
104
+ ]
105
+ typer.echo(
106
+ json.dumps(
107
+ {"results": results, "snapshot": snapshot.digest, "metadataOnly": True},
108
+ indent=2,
109
+ ensure_ascii=False,
110
+ )
111
+ )
112
+ return
113
+ for match in matches:
114
+ card = match.entry.data
115
+ typer.echo(f"{match.entry.display_name}: {card.description}")
116
+ typer.echo(f" source: {card.source.uri}")
117
+ typer.echo(f" path: {card.source.path}")
118
+ typer.echo(f" revision: {card.source.revision}")
119
+ typer.echo(f" license: {card.license}; artifact: {card.artifact_availability}")
120
+ typer.echo(f" identifier: {match.entry.identifier}")
121
+ typer.echo("Metadata only. Execution approval remains a separate decision.")