openenv 0.4.1__tar.gz → 0.4.2__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.1/src/openenv.egg-info → openenv-0.4.2}/PKG-INFO +2 -2
- {openenv-0.4.1 → openenv-0.4.2}/pyproject.toml +2 -2
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/commands/build.py +26 -9
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/commands/collect.py +22 -21
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/commands/init.py +16 -10
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/commands/push.py +66 -51
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/importers/base.py +36 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/importers/ors.py +5 -26
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/importers/verifiers.py +5 -26
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/README.md +2 -2
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/client.py +2 -2
- openenv-0.4.2/src/openenv/core/containers/runtime/_server_config.py +53 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/containers/runtime/daytona_provider.py +3 -42
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/containers/runtime/modal_provider.py +3 -41
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_client.py +249 -66
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/interfaces.py +82 -6
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/web_interface.py +69 -38
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/generic_client.py +3 -3
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/harness/__init__.py +13 -4
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/llm_client.py +14 -14
- {openenv-0.4.1 → openenv-0.4.2/src/openenv.egg-info}/PKG-INFO +2 -2
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv.egg-info/SOURCES.txt +1 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv.egg-info/requires.txt +1 -1
- {openenv-0.4.1 → openenv-0.4.2}/LICENSE +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/MANIFEST.in +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/README.md +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/setup.cfg +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/__init__.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/auto/__init__.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/auto/_discovery.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/auto/auto_action.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/auto/auto_env.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/__init__.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/__main__.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/_cli_utils.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/_validation.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/commands/__init__.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/commands/fork.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/commands/import_env.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/commands/serve.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/commands/skills.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/commands/validate.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/importers/__init__.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/importers/templates/__init__.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/importers/templates/ors_app.py.tpl +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/importers/templates/ors_environment.py.tpl +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/importers/templates/verifiers_app.py.tpl +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/importers/templates/verifiers_environment.py.tpl +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/__init__.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/__init__.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/models.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/openenv.yaml +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/pyproject.toml +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/server/Dockerfile +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/server/__ENV_NAME___environment.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/server/__init__.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/server/app.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/server/requirements.txt +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/__init__.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/client_types.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/containers/__init__.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/containers/runtime/__init__.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/containers/runtime/aca_provider.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/containers/runtime/hf_sandbox_provider.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/containers/runtime/providers.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/containers/runtime/uv_provider.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/containers/test_local_docker_provider.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/__init__.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/_utils.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/base_transforms.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/exceptions.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/gradio_theme.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/gradio_ui.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/http_server.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/mcp_environment.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/mcp_types.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/route_config.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/serialization.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/types.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/evals/__init__.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/evals/base.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/evals/inspect_harness.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/evals/types.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/harness/collect.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/mcp_client.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/rubrics/__init__.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/rubrics/base.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/rubrics/containers.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/rubrics/llm_judge.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/rubrics/trajectory.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/sync_client.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/tools/__init__.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/tools/git_server_client.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/tools/local_python_executor.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/utils.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv.egg-info/dependency_links.txt +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv.egg-info/entry_points.txt +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv.egg-info/top_level.txt +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/src/openenv_core/__init__.py +0 -0
- {openenv-0.4.1 → openenv-0.4.2}/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.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: A unified framework for reinforcement learning environments
|
|
5
5
|
License-Expression: BSD-3-Clause
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -19,7 +19,7 @@ Requires-Dist: openai>=2.7.2
|
|
|
19
19
|
Requires-Dist: tomli>=2.3.0
|
|
20
20
|
Requires-Dist: tomli-w>=1.2.0
|
|
21
21
|
Requires-Dist: websockets>=15.0.1
|
|
22
|
-
Requires-Dist: fastmcp
|
|
22
|
+
Requires-Dist: fastmcp<4.0.0,>=3.0.0
|
|
23
23
|
Requires-Dist: gradio>=4.0.0
|
|
24
24
|
Requires-Dist: httpx>=0.28.1
|
|
25
25
|
Provides-Extra: daytona
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "openenv"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.2"
|
|
8
8
|
description = "A unified framework for reinforcement learning environments"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "BSD-3-Clause"
|
|
@@ -29,7 +29,7 @@ dependencies = [
|
|
|
29
29
|
"tomli-w>=1.2.0",
|
|
30
30
|
"websockets>=15.0.1",
|
|
31
31
|
# MCP support
|
|
32
|
-
"fastmcp>=3.0.0",
|
|
32
|
+
"fastmcp>=3.0.0,<4.0.0",
|
|
33
33
|
# Web UI dependencies
|
|
34
34
|
"gradio>=4.0.0",
|
|
35
35
|
"httpx>=0.28.1",
|
|
@@ -293,19 +293,36 @@ def _build_docker_image(
|
|
|
293
293
|
build_args["BUILD_MODE"] = build_mode
|
|
294
294
|
build_args["ENV_NAME"] = env_path.name.replace("_env", "")
|
|
295
295
|
|
|
296
|
-
|
|
297
|
-
|
|
296
|
+
cmd = _docker_build_command(
|
|
297
|
+
tag=tag,
|
|
298
|
+
dockerfile=dockerfile,
|
|
299
|
+
build_dir=build_dir,
|
|
300
|
+
build_args=build_args,
|
|
301
|
+
no_cache=no_cache,
|
|
302
|
+
)
|
|
298
303
|
|
|
299
|
-
|
|
300
|
-
|
|
304
|
+
result = _run_command(cmd, check=False)
|
|
305
|
+
return result.returncode == 0
|
|
301
306
|
|
|
302
|
-
for key, value in build_args.items():
|
|
303
|
-
cmd.extend(["--build-arg", f"{key}={value}"])
|
|
304
307
|
|
|
305
|
-
|
|
308
|
+
def _docker_build_command(
|
|
309
|
+
tag: str,
|
|
310
|
+
dockerfile: Path,
|
|
311
|
+
build_dir: Path,
|
|
312
|
+
build_args: dict[str, str],
|
|
313
|
+
no_cache: bool,
|
|
314
|
+
) -> list[str]:
|
|
315
|
+
"""Build the docker build command for an OpenEnv environment."""
|
|
316
|
+
cmd = ["docker", "build", "-t", tag, "-f", str(dockerfile)]
|
|
306
317
|
|
|
307
|
-
|
|
308
|
-
|
|
318
|
+
if no_cache:
|
|
319
|
+
cmd.append("--no-cache")
|
|
320
|
+
|
|
321
|
+
for key, value in build_args.items():
|
|
322
|
+
cmd.extend(["--build-arg", f"{key}={value}"])
|
|
323
|
+
|
|
324
|
+
cmd.append(str(build_dir))
|
|
325
|
+
return cmd
|
|
309
326
|
|
|
310
327
|
|
|
311
328
|
def _push_docker_image(tag: str, registry: str | None = None) -> bool:
|
|
@@ -86,6 +86,28 @@ def _resolve_api_key(provider: str) -> str:
|
|
|
86
86
|
)
|
|
87
87
|
|
|
88
88
|
|
|
89
|
+
def _extract_legal_actions(messages: list[dict[str, Any]]) -> list[int]:
|
|
90
|
+
for message in reversed(messages):
|
|
91
|
+
content = message.get("content") or ""
|
|
92
|
+
if not isinstance(content, str):
|
|
93
|
+
continue
|
|
94
|
+
try:
|
|
95
|
+
payload = json.loads(content)
|
|
96
|
+
except (json.JSONDecodeError, ValueError):
|
|
97
|
+
payload = None
|
|
98
|
+
if isinstance(payload, dict) and "legal_actions" in payload:
|
|
99
|
+
legal = payload["legal_actions"]
|
|
100
|
+
if isinstance(legal, list):
|
|
101
|
+
return [int(a) for a in legal]
|
|
102
|
+
match = re.search(r"Legal actions:\s*\[([^\]]*)\]", content)
|
|
103
|
+
if match:
|
|
104
|
+
raw = match.group(1).strip()
|
|
105
|
+
if not raw:
|
|
106
|
+
return []
|
|
107
|
+
return [int(x) for x in raw.split(",")]
|
|
108
|
+
return []
|
|
109
|
+
|
|
110
|
+
|
|
89
111
|
def _build_scripted_model_step():
|
|
90
112
|
"""Default teacher: pick the first legal action from the latest observation.
|
|
91
113
|
|
|
@@ -93,27 +115,6 @@ def _build_scripted_model_step():
|
|
|
93
115
|
users smoke-test the CLI without any API key configured.
|
|
94
116
|
"""
|
|
95
117
|
|
|
96
|
-
def _extract_legal_actions(messages: list[dict[str, Any]]) -> list[int]:
|
|
97
|
-
for message in reversed(messages):
|
|
98
|
-
content = message.get("content") or ""
|
|
99
|
-
if not isinstance(content, str):
|
|
100
|
-
continue
|
|
101
|
-
try:
|
|
102
|
-
payload = json.loads(content)
|
|
103
|
-
except (json.JSONDecodeError, ValueError):
|
|
104
|
-
payload = None
|
|
105
|
-
if isinstance(payload, dict) and "legal_actions" in payload:
|
|
106
|
-
legal = payload["legal_actions"]
|
|
107
|
-
if isinstance(legal, list):
|
|
108
|
-
return [int(a) for a in legal]
|
|
109
|
-
match = re.search(r"Legal actions:\s*\[([^\]]*)\]", content)
|
|
110
|
-
if match:
|
|
111
|
-
raw = match.group(1).strip()
|
|
112
|
-
if not raw:
|
|
113
|
-
return []
|
|
114
|
-
return [int(x) for x in raw.split(",")]
|
|
115
|
-
return []
|
|
116
|
-
|
|
117
118
|
def model_step(messages, tools, sampling):
|
|
118
119
|
del sampling
|
|
119
120
|
legal = _extract_legal_actions(messages)
|
|
@@ -298,16 +298,8 @@ def _copy_and_template_file(
|
|
|
298
298
|
) from e
|
|
299
299
|
|
|
300
300
|
|
|
301
|
-
def
|
|
302
|
-
|
|
303
|
-
template_dir: str,
|
|
304
|
-
dest_dir: Path,
|
|
305
|
-
replacements: Dict[str, str],
|
|
306
|
-
env_name: str,
|
|
307
|
-
) -> List[Path]:
|
|
308
|
-
"""Recursively copy template directory and apply replacements."""
|
|
309
|
-
created_files: List[Path] = []
|
|
310
|
-
|
|
301
|
+
def _resolve_template_path(template_pkg: str, template_dir: str) -> Path:
|
|
302
|
+
"""Resolve the template package directory used by `openenv init`."""
|
|
311
303
|
# Get the package path using importlib.resources but avoid importing the template package
|
|
312
304
|
# We'll use the package's __file__ to get the directory path
|
|
313
305
|
import importlib
|
|
@@ -341,6 +333,20 @@ def _copy_template_directory(
|
|
|
341
333
|
f"Template directory not found: {template_pkg}.{template_dir}"
|
|
342
334
|
)
|
|
343
335
|
|
|
336
|
+
return template_path
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _copy_template_directory(
|
|
340
|
+
template_pkg: str,
|
|
341
|
+
template_dir: str,
|
|
342
|
+
dest_dir: Path,
|
|
343
|
+
replacements: Dict[str, str],
|
|
344
|
+
env_name: str,
|
|
345
|
+
) -> List[Path]:
|
|
346
|
+
"""Recursively copy template directory and apply replacements."""
|
|
347
|
+
created_files: List[Path] = []
|
|
348
|
+
template_path = _resolve_template_path(template_pkg, template_dir)
|
|
349
|
+
|
|
344
350
|
# Walk through all files in template directory using Path
|
|
345
351
|
for item in template_path.rglob("*"):
|
|
346
352
|
if item.is_file():
|
|
@@ -239,6 +239,64 @@ def _validate_openenv_directory(directory: Path) -> tuple[str, dict]:
|
|
|
239
239
|
return env_name, manifest
|
|
240
240
|
|
|
241
241
|
|
|
242
|
+
def _rewrite_dockerfile_for_space(
|
|
243
|
+
dockerfile_content: str,
|
|
244
|
+
*,
|
|
245
|
+
base_image: str | None,
|
|
246
|
+
enable_interface: bool,
|
|
247
|
+
) -> tuple[str, list[str]]:
|
|
248
|
+
"""Rewrite deployment Dockerfile content and return printed change labels."""
|
|
249
|
+
lines = dockerfile_content.split("\n")
|
|
250
|
+
new_lines = []
|
|
251
|
+
cmd_found = False
|
|
252
|
+
base_image_updated = False
|
|
253
|
+
web_interface_env_exists = "ENABLE_WEB_INTERFACE" in dockerfile_content
|
|
254
|
+
last_instruction = None
|
|
255
|
+
|
|
256
|
+
for line in lines:
|
|
257
|
+
stripped = line.strip()
|
|
258
|
+
token = stripped.split(maxsplit=1)[0] if stripped else ""
|
|
259
|
+
current_instruction = token.upper()
|
|
260
|
+
|
|
261
|
+
is_healthcheck_continuation = last_instruction == "HEALTHCHECK"
|
|
262
|
+
|
|
263
|
+
# Update base image if specified
|
|
264
|
+
if base_image and stripped.startswith("FROM") and not base_image_updated:
|
|
265
|
+
new_lines.append(f"FROM {base_image}")
|
|
266
|
+
base_image_updated = True
|
|
267
|
+
last_instruction = "FROM"
|
|
268
|
+
continue
|
|
269
|
+
|
|
270
|
+
if (
|
|
271
|
+
stripped.startswith("CMD")
|
|
272
|
+
and not cmd_found
|
|
273
|
+
and not web_interface_env_exists
|
|
274
|
+
and enable_interface
|
|
275
|
+
and not is_healthcheck_continuation
|
|
276
|
+
):
|
|
277
|
+
new_lines.append("ENV ENABLE_WEB_INTERFACE=true")
|
|
278
|
+
cmd_found = True
|
|
279
|
+
|
|
280
|
+
new_lines.append(line)
|
|
281
|
+
|
|
282
|
+
if current_instruction:
|
|
283
|
+
last_instruction = current_instruction
|
|
284
|
+
|
|
285
|
+
if not cmd_found and not web_interface_env_exists and enable_interface:
|
|
286
|
+
new_lines.append("ENV ENABLE_WEB_INTERFACE=true")
|
|
287
|
+
|
|
288
|
+
if base_image and not base_image_updated:
|
|
289
|
+
new_lines.insert(0, f"FROM {base_image}")
|
|
290
|
+
|
|
291
|
+
changes = []
|
|
292
|
+
if base_image and base_image_updated:
|
|
293
|
+
changes.append("updated base image")
|
|
294
|
+
if enable_interface and not web_interface_env_exists:
|
|
295
|
+
changes.append("enabled web interface")
|
|
296
|
+
|
|
297
|
+
return "\n".join(new_lines), changes
|
|
298
|
+
|
|
299
|
+
|
|
242
300
|
def _get_hf_username() -> str:
|
|
243
301
|
"""Return the authenticated Hugging Face username from whoami()."""
|
|
244
302
|
username = _extract_hf_username(whoami())
|
|
@@ -323,55 +381,12 @@ def _prepare_staging_directory(
|
|
|
323
381
|
# Modify Dockerfile to optionally enable web interface and update base image
|
|
324
382
|
if dockerfile_path and dockerfile_path.exists():
|
|
325
383
|
dockerfile_content = dockerfile_path.read_text()
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
for line in lines:
|
|
334
|
-
stripped = line.strip()
|
|
335
|
-
token = stripped.split(maxsplit=1)[0] if stripped else ""
|
|
336
|
-
current_instruction = token.upper()
|
|
337
|
-
|
|
338
|
-
is_healthcheck_continuation = last_instruction == "HEALTHCHECK"
|
|
339
|
-
|
|
340
|
-
# Update base image if specified
|
|
341
|
-
if base_image and stripped.startswith("FROM") and not base_image_updated:
|
|
342
|
-
new_lines.append(f"FROM {base_image}")
|
|
343
|
-
base_image_updated = True
|
|
344
|
-
last_instruction = "FROM"
|
|
345
|
-
continue
|
|
346
|
-
|
|
347
|
-
if (
|
|
348
|
-
stripped.startswith("CMD")
|
|
349
|
-
and not cmd_found
|
|
350
|
-
and not web_interface_env_exists
|
|
351
|
-
and enable_interface
|
|
352
|
-
and not is_healthcheck_continuation
|
|
353
|
-
):
|
|
354
|
-
new_lines.append("ENV ENABLE_WEB_INTERFACE=true")
|
|
355
|
-
cmd_found = True
|
|
356
|
-
|
|
357
|
-
new_lines.append(line)
|
|
358
|
-
|
|
359
|
-
if current_instruction:
|
|
360
|
-
last_instruction = current_instruction
|
|
361
|
-
|
|
362
|
-
if not cmd_found and not web_interface_env_exists and enable_interface:
|
|
363
|
-
new_lines.append("ENV ENABLE_WEB_INTERFACE=true")
|
|
364
|
-
|
|
365
|
-
if base_image and not base_image_updated:
|
|
366
|
-
new_lines.insert(0, f"FROM {base_image}")
|
|
367
|
-
|
|
368
|
-
dockerfile_path.write_text("\n".join(new_lines))
|
|
369
|
-
|
|
370
|
-
changes = []
|
|
371
|
-
if base_image and base_image_updated:
|
|
372
|
-
changes.append("updated base image")
|
|
373
|
-
if enable_interface and not web_interface_env_exists:
|
|
374
|
-
changes.append("enabled web interface")
|
|
384
|
+
dockerfile_content, changes = _rewrite_dockerfile_for_space(
|
|
385
|
+
dockerfile_content,
|
|
386
|
+
base_image=base_image,
|
|
387
|
+
enable_interface=enable_interface,
|
|
388
|
+
)
|
|
389
|
+
dockerfile_path.write_text(dockerfile_content)
|
|
375
390
|
if changes:
|
|
376
391
|
console.print(
|
|
377
392
|
f"[bold green]✓[/bold green] Updated Dockerfile: {', '.join(changes)}"
|
|
@@ -406,8 +421,8 @@ def _prepare_staging_directory(
|
|
|
406
421
|
frontmatter = f"""---
|
|
407
422
|
title: {env_name.replace("_", " ").title()} Environment Server
|
|
408
423
|
emoji: 🔊
|
|
409
|
-
colorFrom:
|
|
410
|
-
colorTo:
|
|
424
|
+
colorFrom: blue
|
|
425
|
+
colorTo: indigo
|
|
411
426
|
sdk: docker
|
|
412
427
|
pinned: false
|
|
413
428
|
app_port: 8000
|
|
@@ -257,6 +257,42 @@ class DetectedEnvironment:
|
|
|
257
257
|
return f"{self.module_path}:{self.class_name}"
|
|
258
258
|
|
|
259
259
|
|
|
260
|
+
@dataclass(frozen=True)
|
|
261
|
+
class ImportPackageContext:
|
|
262
|
+
"""Shared details produced while scaffolding an imported environment package."""
|
|
263
|
+
|
|
264
|
+
class_name_prefix: str
|
|
265
|
+
vendor_dir: str
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def prepare_import_package(
|
|
269
|
+
source: Path, destination: Path, env_name: str
|
|
270
|
+
) -> ImportPackageContext:
|
|
271
|
+
from openenv.cli.commands.init import (
|
|
272
|
+
_copy_template_directory,
|
|
273
|
+
_create_template_replacements,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
replacements = _create_template_replacements(env_name)
|
|
277
|
+
_copy_template_directory(
|
|
278
|
+
"openenv.cli.templates.openenv_env",
|
|
279
|
+
"",
|
|
280
|
+
destination,
|
|
281
|
+
replacements,
|
|
282
|
+
env_name,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
vendor_dir = safe_vendor_dir_name(source)
|
|
286
|
+
vendor_path = destination / "vendor" / vendor_dir
|
|
287
|
+
copy_source_tree(source, vendor_path)
|
|
288
|
+
ensure_vendor_package(vendor_path)
|
|
289
|
+
|
|
290
|
+
return ImportPackageContext(
|
|
291
|
+
class_name_prefix=replacements["__ENV_CLASS_NAME__"],
|
|
292
|
+
vendor_dir=vendor_dir,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
260
296
|
class EnvironmentImporter(Protocol):
|
|
261
297
|
source_type: str
|
|
262
298
|
|
|
@@ -8,13 +8,11 @@ from pathlib import Path
|
|
|
8
8
|
from .base import (
|
|
9
9
|
append_dependency_files,
|
|
10
10
|
collect_source_dependencies,
|
|
11
|
-
copy_source_tree,
|
|
12
11
|
DetectedEnvironment,
|
|
13
|
-
ensure_vendor_package,
|
|
14
12
|
iter_python_files,
|
|
15
13
|
module_path,
|
|
14
|
+
prepare_import_package,
|
|
16
15
|
render_importer_template,
|
|
17
|
-
safe_vendor_dir_name,
|
|
18
16
|
write_text,
|
|
19
17
|
)
|
|
20
18
|
|
|
@@ -209,39 +207,20 @@ class ORSImporter:
|
|
|
209
207
|
env_name: str,
|
|
210
208
|
detected: DetectedEnvironment,
|
|
211
209
|
) -> None:
|
|
212
|
-
|
|
213
|
-
_copy_template_directory,
|
|
214
|
-
_create_template_replacements,
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
replacements = _create_template_replacements(env_name)
|
|
218
|
-
_copy_template_directory(
|
|
219
|
-
"openenv.cli.templates.openenv_env",
|
|
220
|
-
"",
|
|
221
|
-
destination,
|
|
222
|
-
replacements,
|
|
223
|
-
env_name,
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
vendor_dir = safe_vendor_dir_name(source)
|
|
227
|
-
vendor_path = destination / "vendor" / vendor_dir
|
|
228
|
-
copy_source_tree(source, vendor_path)
|
|
229
|
-
ensure_vendor_package(vendor_path)
|
|
230
|
-
|
|
231
|
-
prefix = replacements["__ENV_CLASS_NAME__"]
|
|
210
|
+
package = prepare_import_package(source, destination, env_name)
|
|
232
211
|
write_text(
|
|
233
212
|
destination / "server" / f"{env_name}_environment.py",
|
|
234
213
|
_wrapper_source(
|
|
235
214
|
env_name=env_name,
|
|
236
|
-
class_name_prefix=
|
|
215
|
+
class_name_prefix=package.class_name_prefix,
|
|
237
216
|
source_module=detected.module_path,
|
|
238
217
|
source_class=detected.class_name,
|
|
239
|
-
vendor_dir=vendor_dir,
|
|
218
|
+
vendor_dir=package.vendor_dir,
|
|
240
219
|
),
|
|
241
220
|
)
|
|
242
221
|
write_text(
|
|
243
222
|
destination / "server" / "app.py",
|
|
244
|
-
_app_source(env_name=env_name, class_name_prefix=
|
|
223
|
+
_app_source(env_name=env_name, class_name_prefix=package.class_name_prefix),
|
|
245
224
|
)
|
|
246
225
|
dependencies = collect_source_dependencies(source)
|
|
247
226
|
for dependency in detect_ors_dependencies(source):
|
|
@@ -8,13 +8,11 @@ from pathlib import Path
|
|
|
8
8
|
from .base import (
|
|
9
9
|
append_dependency_files,
|
|
10
10
|
collect_source_dependencies,
|
|
11
|
-
copy_source_tree,
|
|
12
11
|
DetectedEnvironment,
|
|
13
|
-
ensure_vendor_package,
|
|
14
12
|
iter_python_files,
|
|
15
13
|
module_path,
|
|
14
|
+
prepare_import_package,
|
|
16
15
|
render_importer_template,
|
|
17
|
-
safe_vendor_dir_name,
|
|
18
16
|
write_text,
|
|
19
17
|
)
|
|
20
18
|
|
|
@@ -110,38 +108,19 @@ class VerifiersImporter:
|
|
|
110
108
|
env_name: str,
|
|
111
109
|
detected: DetectedEnvironment,
|
|
112
110
|
) -> None:
|
|
113
|
-
|
|
114
|
-
_copy_template_directory,
|
|
115
|
-
_create_template_replacements,
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
replacements = _create_template_replacements(env_name)
|
|
119
|
-
_copy_template_directory(
|
|
120
|
-
"openenv.cli.templates.openenv_env",
|
|
121
|
-
"",
|
|
122
|
-
destination,
|
|
123
|
-
replacements,
|
|
124
|
-
env_name,
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
vendor_dir = safe_vendor_dir_name(source)
|
|
128
|
-
vendor_path = destination / "vendor" / vendor_dir
|
|
129
|
-
copy_source_tree(source, vendor_path)
|
|
130
|
-
ensure_vendor_package(vendor_path)
|
|
131
|
-
|
|
132
|
-
prefix = replacements["__ENV_CLASS_NAME__"]
|
|
111
|
+
package = prepare_import_package(source, destination, env_name)
|
|
133
112
|
write_text(
|
|
134
113
|
destination / "server" / f"{env_name}_environment.py",
|
|
135
114
|
_wrapper_source(
|
|
136
115
|
env_name=env_name,
|
|
137
|
-
class_name_prefix=
|
|
116
|
+
class_name_prefix=package.class_name_prefix,
|
|
138
117
|
source_module=detected.module_path,
|
|
139
|
-
vendor_dir=vendor_dir,
|
|
118
|
+
vendor_dir=package.vendor_dir,
|
|
140
119
|
),
|
|
141
120
|
)
|
|
142
121
|
write_text(
|
|
143
122
|
destination / "server" / "app.py",
|
|
144
|
-
_app_source(env_name=env_name, class_name_prefix=
|
|
123
|
+
_app_source(env_name=env_name, class_name_prefix=package.class_name_prefix),
|
|
145
124
|
)
|
|
146
125
|
dependencies = collect_source_dependencies(source)
|
|
147
126
|
if "verifiers>=0.1.14" not in dependencies:
|
|
@@ -23,8 +23,8 @@ The simplest way to use the __ENV_TITLE_NAME__ environment is through the `__ENV
|
|
|
23
23
|
from __ENV_NAME__ import __ENV_CLASS_NAME__Action, __ENV_CLASS_NAME__Env
|
|
24
24
|
|
|
25
25
|
try:
|
|
26
|
-
# Create environment from Docker image
|
|
27
|
-
__ENV_NAME__env = __ENV_CLASS_NAME__Env.from_docker_image("__ENV_NAME__-env:latest")
|
|
26
|
+
# Create environment from Docker image (.sync() for synchronous use)
|
|
27
|
+
__ENV_NAME__env = __ENV_CLASS_NAME__Env.from_docker_image("__ENV_NAME__-env:latest").sync()
|
|
28
28
|
|
|
29
29
|
# Reset
|
|
30
30
|
result = __ENV_NAME__env.reset()
|
|
@@ -31,8 +31,8 @@ class __ENV_CLASS_NAME__Env(
|
|
|
31
31
|
... print(result.observation.echoed_message)
|
|
32
32
|
|
|
33
33
|
Example with Docker:
|
|
34
|
-
>>> # Automatically start container and connect
|
|
35
|
-
>>> client = __ENV_CLASS_NAME__Env.from_docker_image("__ENV_NAME__-env:latest")
|
|
34
|
+
>>> # Automatically start container and connect (.sync() for sync use)
|
|
35
|
+
>>> client = __ENV_CLASS_NAME__Env.from_docker_image("__ENV_NAME__-env:latest").sync()
|
|
36
36
|
>>> try:
|
|
37
37
|
... result = client.reset()
|
|
38
38
|
... result = client.step(__ENV_CLASS_NAME__Action(message="Test"))
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
2
|
+
|
|
3
|
+
"""Shared helpers for reading environment server configuration."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_openenv_app_field(yaml_content: str) -> str | None:
|
|
14
|
+
"""Extract the top-level ``app`` value from raw ``openenv.yaml`` content."""
|
|
15
|
+
try:
|
|
16
|
+
data = yaml.safe_load(yaml_content) or {}
|
|
17
|
+
except Exception:
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
if not isinstance(data, dict):
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
value = data.get("app")
|
|
24
|
+
if isinstance(value, str):
|
|
25
|
+
value = value.strip()
|
|
26
|
+
return value if value else None
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def parse_dockerfile_cmd(dockerfile_content: str) -> str | None:
|
|
31
|
+
"""Extract the server command from the last Dockerfile ``CMD``."""
|
|
32
|
+
last_cmd: str | None = None
|
|
33
|
+
for line in dockerfile_content.splitlines():
|
|
34
|
+
stripped = line.strip()
|
|
35
|
+
if stripped.startswith("#"):
|
|
36
|
+
continue
|
|
37
|
+
match = re.match(r"CMD\s+(.+)", stripped, flags=re.IGNORECASE)
|
|
38
|
+
if match:
|
|
39
|
+
last_cmd = match.group(1).strip()
|
|
40
|
+
|
|
41
|
+
if last_cmd is None:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
if last_cmd.startswith("["):
|
|
45
|
+
try:
|
|
46
|
+
parts = json.loads(last_cmd)
|
|
47
|
+
if isinstance(parts, list) and all(isinstance(p, str) for p in parts):
|
|
48
|
+
return " ".join(parts)
|
|
49
|
+
except (json.JSONDecodeError, TypeError):
|
|
50
|
+
# Invalid exec-form JSON falls back to the raw CMD below.
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
return last_cmd if last_cmd else None
|
|
@@ -8,14 +8,12 @@ Requires the ``daytona`` SDK: ``pip install daytona>=0.10``
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
-
import json
|
|
12
11
|
import os
|
|
13
12
|
import shlex
|
|
14
13
|
import time
|
|
15
14
|
from typing import Any, Callable, Dict, Optional
|
|
16
15
|
|
|
17
|
-
import
|
|
18
|
-
|
|
16
|
+
from ._server_config import parse_dockerfile_cmd, parse_openenv_app_field
|
|
19
17
|
from .providers import ContainerProvider
|
|
20
18
|
|
|
21
19
|
|
|
@@ -149,19 +147,7 @@ class DaytonaProvider(ContainerProvider):
|
|
|
149
147
|
|
|
150
148
|
Uses PyYAML to handle comments, quotes, and nested keys correctly.
|
|
151
149
|
"""
|
|
152
|
-
|
|
153
|
-
data = yaml.safe_load(yaml_content) or {}
|
|
154
|
-
except Exception:
|
|
155
|
-
return None
|
|
156
|
-
|
|
157
|
-
if not isinstance(data, dict):
|
|
158
|
-
return None
|
|
159
|
-
|
|
160
|
-
value = data.get("app")
|
|
161
|
-
if isinstance(value, str):
|
|
162
|
-
value = value.strip()
|
|
163
|
-
return value if value else None
|
|
164
|
-
return None
|
|
150
|
+
return parse_openenv_app_field(yaml_content)
|
|
165
151
|
|
|
166
152
|
@staticmethod
|
|
167
153
|
def _parse_dockerfile_cmd(dockerfile_content: str) -> Optional[str]:
|
|
@@ -176,32 +162,7 @@ class DaytonaProvider(ContainerProvider):
|
|
|
176
162
|
Returns:
|
|
177
163
|
The command as a single string, or ``None`` if no ``CMD`` found.
|
|
178
164
|
"""
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
last_cmd: Optional[str] = None
|
|
182
|
-
for line in dockerfile_content.splitlines():
|
|
183
|
-
stripped = line.strip()
|
|
184
|
-
# Skip comments
|
|
185
|
-
if stripped.startswith("#"):
|
|
186
|
-
continue
|
|
187
|
-
match = re.match(r"CMD\s+(.+)", stripped, flags=re.IGNORECASE)
|
|
188
|
-
if match:
|
|
189
|
-
last_cmd = match.group(1).strip()
|
|
190
|
-
|
|
191
|
-
if last_cmd is None:
|
|
192
|
-
return None
|
|
193
|
-
|
|
194
|
-
# Exec form: CMD ["executable", "param1", ...]
|
|
195
|
-
if last_cmd.startswith("["):
|
|
196
|
-
try:
|
|
197
|
-
parts = json.loads(last_cmd)
|
|
198
|
-
if isinstance(parts, list) and all(isinstance(p, str) for p in parts):
|
|
199
|
-
return " ".join(parts)
|
|
200
|
-
except (json.JSONDecodeError, TypeError):
|
|
201
|
-
pass
|
|
202
|
-
|
|
203
|
-
# Shell form: CMD executable param1 ...
|
|
204
|
-
return last_cmd if last_cmd else None
|
|
165
|
+
return parse_dockerfile_cmd(dockerfile_content)
|
|
205
166
|
|
|
206
167
|
@staticmethod
|
|
207
168
|
def strip_buildkit_syntax(dockerfile_content: str) -> str:
|