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.
- {openenv-0.4.2/src/openenv.egg-info → openenv-0.5.0}/PKG-INFO +6 -3
- {openenv-0.4.2 → openenv-0.5.0}/README.md +2 -2
- {openenv-0.4.2 → openenv-0.5.0}/pyproject.toml +16 -1
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/auto/_discovery.py +159 -9
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/__main__.py +12 -3
- openenv-0.5.0/src/openenv/cli/commands/catalog.py +121 -0
- openenv-0.5.0/src/openenv/cli/commands/harbor.py +715 -0
- openenv-0.5.0/src/openenv/cli/commands/validate.py +247 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/templates/openenv_env/README.md +8 -2
- openenv-0.5.0/src/openenv/cli/templates/openenv_env/openenv.yaml +23 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/templates/openenv_env/server/__ENV_NAME___environment.py +2 -2
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_client.py +181 -20
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/http_server.py +99 -16
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/serialization.py +13 -3
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/web_interface.py +4 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/harness/__init__.py +101 -1
- openenv-0.5.0/src/openenv/core/harness/capture/__init__.py +37 -0
- openenv-0.5.0/src/openenv/core/harness/capture/compat.py +287 -0
- openenv-0.5.0/src/openenv/core/harness/capture/contract.py +331 -0
- openenv-0.5.0/src/openenv/core/harness/capture/detection.py +65 -0
- openenv-0.5.0/src/openenv/core/harness/capture/dialects/__init__.py +23 -0
- openenv-0.5.0/src/openenv/core/harness/capture/dialects/anthropic.py +705 -0
- openenv-0.5.0/src/openenv/core/harness/capture/dialects/base.py +134 -0
- openenv-0.5.0/src/openenv/core/harness/capture/dialects/google.py +729 -0
- openenv-0.5.0/src/openenv/core/harness/capture/dialects/images.py +335 -0
- openenv-0.5.0/src/openenv/core/harness/capture/dialects/openai_chat.py +41 -0
- openenv-0.5.0/src/openenv/core/harness/capture/dialects/openai_responses.py +1125 -0
- openenv-0.5.0/src/openenv/core/harness/capture/dialects/reasoning.py +125 -0
- openenv-0.5.0/src/openenv/core/harness/capture/export.py +246 -0
- openenv-0.5.0/src/openenv/core/harness/capture/forwarding.py +346 -0
- openenv-0.5.0/src/openenv/core/harness/capture/graph.py +496 -0
- openenv-0.5.0/src/openenv/core/harness/capture/providers.py +325 -0
- openenv-0.5.0/src/openenv/core/harness/capture/runner.py +179 -0
- openenv-0.5.0/src/openenv/core/harness/capture/server.py +1474 -0
- openenv-0.5.0/src/openenv/core/harness/capture/sessions.py +280 -0
- openenv-0.5.0/src/openenv/core/harness/capture/sse.py +255 -0
- openenv-0.5.0/src/openenv/core/harness/capture/upstream.py +603 -0
- openenv-0.5.0/src/openenv/core/harness/capture/validate.py +455 -0
- openenv-0.5.0/src/openenv/core/harness/capture/validate_llm.py +768 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/harness/collect.py +57 -14
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/mcp_client.py +93 -27
- openenv-0.5.0/src/openenv/discovery/__init__.py +34 -0
- openenv-0.5.0/src/openenv/discovery/errors.py +5 -0
- openenv-0.5.0/src/openenv/discovery/metadata.py +191 -0
- openenv-0.5.0/src/openenv/discovery/models.py +425 -0
- openenv-0.5.0/src/openenv/discovery/producer.py +309 -0
- openenv-0.5.0/src/openenv/discovery/repository.py +148 -0
- openenv-0.5.0/src/openenv/discovery/schemas/0.1-draft/catalog.schema.json +715 -0
- openenv-0.5.0/src/openenv/discovery/schemas/0.1-draft/declaration.schema.json +171 -0
- openenv-0.5.0/src/openenv/discovery/schemas/0.1-draft/environment-card.schema.json +396 -0
- openenv-0.5.0/src/openenv/discovery/search.py +131 -0
- openenv-0.5.0/src/openenv/discovery/serialization.py +127 -0
- openenv-0.5.0/src/openenv/harbor/__init__.py +18 -0
- openenv-0.5.0/src/openenv/harbor/atif.py +563 -0
- openenv-0.5.0/src/openenv/harbor/capabilities.py +316 -0
- openenv-0.5.0/src/openenv/harbor/client.py +216 -0
- openenv-0.5.0/src/openenv/harbor/contract.py +193 -0
- openenv-0.5.0/src/openenv/harbor/e2b_stream.py +64 -0
- openenv-0.5.0/src/openenv/harbor/environment.py +338 -0
- openenv-0.5.0/src/openenv/harbor/install_fixes.py +698 -0
- openenv-0.5.0/src/openenv/harbor/models.py +352 -0
- openenv-0.5.0/src/openenv/harbor/nemo_profile.py +26 -0
- openenv-0.5.0/src/openenv/harbor/proc_env_context.py +133 -0
- openenv-0.5.0/src/openenv/harbor/qualification.py +154 -0
- openenv-0.5.0/src/openenv/harbor/rollout.py +777 -0
- openenv-0.5.0/src/openenv/harbor/runner.py +228 -0
- openenv-0.5.0/src/openenv/harbor/seams.py +966 -0
- openenv-0.5.0/src/openenv/harbor/serving.py +324 -0
- openenv-0.5.0/src/openenv/harbor/shared_template.py +106 -0
- openenv-0.5.0/src/openenv/harbor/startup.py +212 -0
- openenv-0.5.0/src/openenv/harbor/tasks.py +302 -0
- openenv-0.5.0/src/openenv/harbor/ui.py +1511 -0
- openenv-0.5.0/src/openenv/validation/__init__.py +94 -0
- openenv-0.5.0/src/openenv/validation/graders/__init__.py +165 -0
- openenv-0.5.0/src/openenv/validation/graders/static/__init__.py +5 -0
- openenv-0.5.0/src/openenv/validation/graders/static/manifest.py +72 -0
- openenv-0.5.0/src/openenv/validation/manifest.py +306 -0
- openenv-0.5.0/src/openenv/validation/parsers/__init__.py +69 -0
- openenv-0.5.0/src/openenv/validation/parsers/openenv_yaml.py +96 -0
- openenv-0.5.0/src/openenv/validation/policies/severity-v1.json +56 -0
- openenv-0.5.0/src/openenv/validation/policy.py +149 -0
- openenv-0.5.0/src/openenv/validation/providers/__init__.py +64 -0
- openenv-0.5.0/src/openenv/validation/report.py +90 -0
- openenv-0.5.0/src/openenv/validation/runner.py +150 -0
- openenv-0.5.0/src/openenv/validation/schemas/manifest.schema.json +430 -0
- openenv-0.5.0/src/openenv/validation/schemas/report.schema.json +591 -0
- openenv-0.5.0/src/openenv/validation/signature.py +120 -0
- openenv-0.5.0/src/openenv/validation/types.py +88 -0
- {openenv-0.4.2 → openenv-0.5.0/src/openenv.egg-info}/PKG-INFO +6 -3
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv.egg-info/SOURCES.txt +72 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv.egg-info/requires.txt +6 -0
- openenv-0.4.2/src/openenv/cli/commands/validate.py +0 -197
- openenv-0.4.2/src/openenv/cli/templates/openenv_env/openenv.yaml +0 -7
- {openenv-0.4.2 → openenv-0.5.0}/LICENSE +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/MANIFEST.in +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/setup.cfg +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/__init__.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/auto/__init__.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/auto/auto_action.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/auto/auto_env.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/__init__.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/_cli_utils.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/_validation.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/commands/__init__.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/commands/build.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/commands/collect.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/commands/fork.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/commands/import_env.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/commands/init.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/commands/push.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/commands/serve.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/commands/skills.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/importers/__init__.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/importers/base.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/importers/ors.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/importers/templates/__init__.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/importers/templates/ors_app.py.tpl +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/importers/templates/ors_environment.py.tpl +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/importers/templates/verifiers_app.py.tpl +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/importers/templates/verifiers_environment.py.tpl +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/importers/verifiers.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/templates/__init__.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/templates/openenv_env/__init__.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/templates/openenv_env/client.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/templates/openenv_env/models.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/templates/openenv_env/pyproject.toml +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/templates/openenv_env/server/Dockerfile +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/templates/openenv_env/server/__init__.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/templates/openenv_env/server/app.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/cli/templates/openenv_env/server/requirements.txt +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/__init__.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/client_types.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/containers/__init__.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/containers/runtime/__init__.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/containers/runtime/_server_config.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/containers/runtime/aca_provider.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/containers/runtime/daytona_provider.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/containers/runtime/hf_sandbox_provider.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/containers/runtime/modal_provider.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/containers/runtime/providers.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/containers/runtime/uv_provider.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/containers/test_local_docker_provider.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/__init__.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/_utils.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/base_transforms.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/exceptions.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/gradio_theme.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/gradio_ui.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/interfaces.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/mcp_environment.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/mcp_types.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/route_config.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/env_server/types.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/evals/__init__.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/evals/base.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/evals/inspect_harness.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/evals/types.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/generic_client.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/llm_client.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/rubrics/__init__.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/rubrics/base.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/rubrics/containers.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/rubrics/llm_judge.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/rubrics/trajectory.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/sync_client.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/tools/__init__.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/tools/git_server_client.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/tools/local_python_executor.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv/core/utils.py +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv.egg-info/dependency_links.txt +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv.egg-info/entry_points.txt +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv.egg-info/top_level.txt +0 -0
- {openenv-0.4.2 → openenv-0.5.0}/src/openenv_core/__init__.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
441
|
-
|
|
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
|
-
"""
|
|
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.
|
|
41
|
-
|
|
42
|
-
|
|
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.")
|