Repository navigation
feat(run_code): run a script on a deploy_sandbox sandbox via sandbox_name #69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
psinghal20
merged 5 commits into
pratyush/container-sandbox-file-routing
from
pratyush/run-code-named-sandbox
Oct 6, 2026
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
fdb6ae7
feat(run_code): run a script on a deploy_sandbox sandbox via sandbox_…
psinghal20 75e465f
Merge remote-tracking branch 'origin/pratyush/container-sandbox-file-…
psinghal20 9dc9d78
test(run_code): remove the container test's image and registry copy o…
psinghal20 8526c4f
fix(run_code): still load stored env_id steps that carry a named agen…
psinghal20 5f88b38
test(run_code): scope image cleanup to this test's own sandbox contai…
psinghal20 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
212 changes: 212 additions & 0 deletions
212
tst/integration/task_step/test_run_code_named_sandbox_local.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,212 @@ | ||
| """run_code on a deploy_sandbox sandbox: files loaded onto it, a script that builds from them, and the result | ||
| collected off it and loaded back by step id, on a local VM-mode sandbox and on a local container-mode sandbox. | ||
|
|
||
| A local VM-mode sandbox maps only `/app` paths onto its work dir, and a script's own file reads are not rewritten, | ||
| so its paths are absolute host paths under the test's tmp dir. The container-mode run needs Docker and a throwaway | ||
| local registry; its paths are inside the container. | ||
| """ | ||
|
|
||
| import io | ||
| import json | ||
| import re | ||
| import shutil | ||
| import socket | ||
| import subprocess | ||
| import tempfile | ||
| import time | ||
| import uuid | ||
| import zipfile | ||
| from pathlib import Path | ||
|
|
||
| import httpx | ||
|
|
||
| import pytest | ||
|
|
||
| from agent_env.artifact import DockerImageArtifact, FileArtifact, FileArtifactUniverse | ||
| from agent_env.artifact.store import reset_artifact_store | ||
| from agent_env.config import configure, reset_config, set_image_store | ||
| from agent_env.store.image_store import LocalRegistryImageStore | ||
| from agent_env.task import Task | ||
| from agent_env.task_step.task_steps.collect_artifacts import CollectArtifactsTaskStep | ||
| from agent_env.task_step.task_steps.deploy_sandbox import DeploySandboxTaskStep | ||
| from agent_env.task_step.task_steps.load_artifact import LoadArtifactTaskStep | ||
| from agent_env.task_step.task_steps.run_code import RunCodeTaskStep | ||
|
|
||
| pytestmark = pytest.mark.integration | ||
|
|
||
| _BUILD_PY = """\ | ||
| import json, pathlib, zipfile | ||
|
|
||
| def run(inp): | ||
| args = inp["args"] | ||
| src, out = pathlib.Path(args["src"]), pathlib.Path(args["out"]) | ||
| out.parent.mkdir(parents=True, exist_ok=True) | ||
| names = sorted(p.name for p in src.iterdir()) | ||
| with zipfile.ZipFile(out, "w") as zf: | ||
| for name in names: | ||
| zf.write(src / name, f"bundle/{name}") | ||
| out.chmod(0o600) | ||
| return {"members": names} | ||
| """ | ||
|
|
||
| _READ_PY = """\ | ||
| import pathlib, zipfile | ||
|
|
||
| def run(inp): | ||
| (path,) = pathlib.Path(inp["args"]["dir"]).iterdir() | ||
| return {"name": path.name, "members": sorted(zipfile.ZipFile(path).namelist())} | ||
| """ | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def local_backends(monkeypatch, tmp_path): | ||
| sandboxes = tmp_path / "sandboxes" | ||
| monkeypatch.chdir(tmp_path) | ||
| monkeypatch.setenv("AGENT_ENV_DOCUMENT_STORE", "local") | ||
| monkeypatch.setenv("AGENT_ENV_OBJECT_STORE", "local") | ||
| monkeypatch.setenv("AGENT_ENV_LOCAL_SANDBOX_DIR", str(sandboxes)) | ||
| configure() | ||
| reset_artifact_store() | ||
| try: | ||
| yield tmp_path | ||
| finally: | ||
| shutil.rmtree(sandboxes, ignore_errors=True) | ||
| reset_artifact_store() | ||
| reset_config() | ||
|
|
||
|
|
||
| def _universe(src_dir, uid, files): | ||
| src_dir.mkdir() | ||
| for name, body in files.items(): | ||
| (src_dir / name).write_text(body) | ||
| return FileArtifactUniverse.put( | ||
| id=uid, | ||
| file_artifacts={ | ||
| name: FileArtifact.put(id=f"{uid}-{name}", description=name, file_path=str(src_dir / name)) | ||
| for name in files | ||
| }, | ||
| ) | ||
|
|
||
|
|
||
| def _chain(tmp_path, suffix, deploy, work): | ||
| inputs = _universe(tmp_path / "inputs", f"inputs-{suffix}", { | ||
| "task.json": json.dumps({"initial_prompt": "hi"}), | ||
| "rubrics.json": "[]", | ||
| }) | ||
| build = _universe(tmp_path / "build", f"build-{suffix}", {"build.py": _BUILD_PY}) | ||
| read = _universe(tmp_path / "read", f"read-{suffix}", {"read.py": _READ_PY}) | ||
| return Task.put(id=f"named-sandbox-{suffix}", steps=[ | ||
| deploy, | ||
| LoadArtifactTaskStep( | ||
| id="load-inputs", version=None, artifact_id=inputs.id, sandbox_name="box", | ||
| destination_path=f"{work}/inputs", | ||
| ), | ||
| RunCodeTaskStep( | ||
| id="build", version=None, script_artifact_id=build.id, sandbox_name="box", | ||
| args={"src": f"{work}/inputs", "out": f"{work}/out/bundle.zip"}, | ||
| ), | ||
| CollectArtifactsTaskStep( | ||
| id="collect", version=None, sandbox_name="box", base_path=f"{work}/out", artifact_paths=["bundle.zip"], | ||
| ), | ||
| LoadArtifactTaskStep( | ||
| id="load-back", version=None, artifact_from_step_id="collect", sandbox_name="box", | ||
| destination_path=f"{work}/loaded", | ||
| ), | ||
| RunCodeTaskStep( | ||
| id="read", version=None, script_artifact_id=read.id, sandbox_name="box", | ||
| args={"dir": f"{work}/loaded"}, | ||
| ), | ||
| ]) | ||
|
|
||
|
|
||
| def _assert_round_trip(ctx): | ||
| results = ctx.metadata["script_results"] | ||
| assert results["build"] == {"members": ["rubrics.json", "task.json"]} | ||
| assert results["read"] == {"name": "bundle.zip", "members": ["bundle/rubrics.json", "bundle/task.json"]} | ||
| universe = FileArtifactUniverse.get(ctx.metadata["collected_artifacts"]["collect"]["file_artifact_universe"]["id"]) | ||
| stored = universe.get_file_artifacts()["bundle.zip"].load() | ||
| assert sorted(zipfile.ZipFile(io.BytesIO(stored)).namelist()) == ["bundle/rubrics.json", "bundle/task.json"] | ||
| assert Task.get_instance(ctx.instance_id).status == "completed" | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_a_script_builds_on_a_named_vm_sandbox_and_its_output_round_trips(local_backends): | ||
| tmp_path = local_backends | ||
| deploy = DeploySandboxTaskStep(id="deploy", version=None, sandbox_name="box", sandbox_mode="vm", sandbox_type="local") | ||
| ctx = await _chain(tmp_path, uuid.uuid4().hex[:8], deploy, str(tmp_path / "work")).run() | ||
| _assert_round_trip(ctx) | ||
|
|
||
|
|
||
| def _docker(*args): | ||
| return subprocess.run(["docker", *args], capture_output=True, text=True) | ||
|
|
||
|
|
||
| def _free_port() -> int: | ||
| with socket.socket() as s: | ||
| s.bind(("127.0.0.1", 0)) | ||
| return s.getsockname()[1] | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def local_registry(local_backends): | ||
| port = _free_port() | ||
| name = f"run-code-registry-{uuid.uuid4().hex[:8]}" | ||
| started = _docker("run", "-d", "--rm", "--name", name, "-p", f"127.0.0.1:{port}:5000", "registry:2") | ||
| assert started.returncode == 0, started.stderr | ||
| host = f"localhost:{port}" | ||
| deadline = time.time() + 45 | ||
| while time.time() < deadline: | ||
| try: | ||
| if httpx.get(f"http://{host}/v2/", timeout=2).status_code in (200, 401): | ||
| break | ||
| except httpx.HTTPError: | ||
| pass | ||
| time.sleep(0.5) | ||
| set_image_store(LocalRegistryImageStore(host)) | ||
| try: | ||
| yield local_backends | ||
| finally: | ||
| _remove_sandbox_containers(local_backends / "sandboxes") | ||
| _docker("rm", "-f", name) | ||
|
|
||
|
|
||
| def _remove_sandbox_containers(sandboxes: Path): | ||
| # Only this test's containers: every local sandbox it deployed has a work dir in its own sandbox root, and its | ||
| # container is named after that sandbox. Matching by image could reach another run's containers. | ||
| for work_dir in sandboxes.glob("agent-env-*"): | ||
| if m := re.match(r"agent-env-(local-[0-9a-f]+)-", work_dir.name): | ||
| _docker("rm", "-f", f"agent-{m.group(1)}") | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_a_script_builds_in_a_named_container_sandbox_and_its_output_round_trips(local_registry): | ||
| tmp_path = local_registry | ||
| suffix = uuid.uuid4().hex[:8] | ||
| port = _free_port() | ||
| with tempfile.TemporaryDirectory() as d: | ||
| # A non-root image user: the steps write as root, so collect has to read as root too. | ||
| (Path(d) / "Dockerfile").write_text( | ||
| f'FROM python:3.12-slim\nUSER nobody\nCMD ["python", "-m", "http.server", "{port}"]\n' | ||
| ) | ||
| built = _docker("build", "-t", f"run-code-box-{suffix}", d) | ||
| assert built.returncode == 0, built.stderr | ||
| image = DockerImageArtifact.put(id=f"run-code-box-{suffix}", description="box", image_name=f"run-code-box-{suffix}") | ||
| deploy = DeploySandboxTaskStep( | ||
| id="deploy", version=None, sandbox_name="box", sandbox_mode="container", sandbox_type="local", | ||
| image=image.image_name, port=port, | ||
| ) | ||
| try: | ||
| ctx = await _chain(tmp_path, suffix, deploy, "/work").run() | ||
| finally: | ||
| # The tag is unique to this run, as is its registry copy: drop them so repeated runs don't pile up images. | ||
| # This test's container still holds them, so remove it first. | ||
| _remove_sandbox_containers(tmp_path / "sandboxes") | ||
| refs = [r for ref in (f"run-code-box-{suffix}", f"*/run-code-box-{suffix}") for r in _docker( | ||
| "image", "ls", "--format", "{{.Repository}}:{{.Tag}}", "--filter", f"reference={ref}", | ||
| ).stdout.split()] | ||
| if refs: | ||
| _docker("rmi", "-f", *refs) | ||
|
|
||
| (deployed,) = ctx.deployed_sandboxes | ||
| assert deployed.sandbox_mode == "container" | ||
| _assert_round_trip(ctx) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.