xqo-docker 3.0.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dudikkocak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: xqo-docker
3
+ Version: 3.0.0
4
+ Summary: Docker sandbox backend for xqo run_code
5
+ Author: dudikkocak
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/dudikkocak/xqo
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: xqo<4,>=3.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=8; extra == "dev"
14
+ Requires-Dist: pytest-timeout>=2.3; extra == "dev"
15
+ Dynamic: license-file
16
+
17
+ # xqo-docker
18
+
19
+ The `docker` sandbox backend for [xqo](https://github.com/dudikkocak/xqo)'s `run_code`: each run happens in a
20
+ throwaway container with no network (unless the user grants it), memory, CPU and process limits, and the host
21
+ user's uid. Python projects get a uv venv inside the image; Node, Go, Rust and .NET projects use their official
22
+ images when the toolchains plugin is installed.
23
+
24
+ ```bash
25
+ pip install xqo-docker
26
+ ```
27
+
28
+ ```json
29
+ "sandbox": {"backend": "docker"}
30
+ ```
31
+
32
+ With the plugin installed, `"backend": "auto"` picks Docker whenever the daemon answers and falls back to `local`
33
+ otherwise. `"plugins": {"disable": ["docker"]}` turns it off without uninstalling. `/api/plugins` shows whether the
34
+ daemon answered (`status.daemon`).
35
+
36
+ Needs the `docker` CLI and a running daemon (Docker Desktop, Docker Engine or a compatible one). The image is set
37
+ with `sandbox.image` (default `astral/uv:python3.12-bookworm-slim`); it needs `uv` and `python` on its PATH.
@@ -0,0 +1,21 @@
1
+ # xqo-docker
2
+
3
+ The `docker` sandbox backend for [xqo](https://github.com/dudikkocak/xqo)'s `run_code`: each run happens in a
4
+ throwaway container with no network (unless the user grants it), memory, CPU and process limits, and the host
5
+ user's uid. Python projects get a uv venv inside the image; Node, Go, Rust and .NET projects use their official
6
+ images when the toolchains plugin is installed.
7
+
8
+ ```bash
9
+ pip install xqo-docker
10
+ ```
11
+
12
+ ```json
13
+ "sandbox": {"backend": "docker"}
14
+ ```
15
+
16
+ With the plugin installed, `"backend": "auto"` picks Docker whenever the daemon answers and falls back to `local`
17
+ otherwise. `"plugins": {"disable": ["docker"]}` turns it off without uninstalling. `/api/plugins` shows whether the
18
+ daemon answered (`status.daemon`).
19
+
20
+ Needs the `docker` CLI and a running daemon (Docker Desktop, Docker Engine or a compatible one). The image is set
21
+ with `sandbox.image` (default `astral/uv:python3.12-bookworm-slim`); it needs `uv` and `python` on its PATH.
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "xqo-docker"
7
+ version = "3.0.0"
8
+ description = "Docker sandbox backend for xqo run_code"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{name = "dudikkocak"}]
13
+ dependencies = [
14
+ "xqo>=3.0,<4",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ dev = ["pytest>=8", "pytest-timeout>=2.3"]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/dudikkocak/xqo"
22
+
23
+ [project.entry-points."xqo.plugins"]
24
+ docker = "xqo_docker:DockerPlugin"
25
+
26
+ [tool.setuptools.packages.find]
27
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,25 @@
1
+ """xqo-docker: the "docker" sandbox backend for run_code (throwaway containers, no network unless granted,
2
+ toolchain images). With it installed, "sandbox": {"backend": "auto"} picks Docker when the daemon answers; without it
3
+ (or with "plugins": {"disable": ["docker"]}) runs use the local backend."""
4
+ from __future__ import annotations
5
+
6
+ import importlib
7
+
8
+ from xqo.api import Plugin, docker_available
9
+
10
+ __version__ = "3.0.0"
11
+
12
+
13
+ class DockerPlugin(Plugin):
14
+ name = "docker"
15
+ description = "Docker sandbox backend for run_code"
16
+
17
+ @classmethod
18
+ def register(cls) -> None:
19
+ importlib.import_module(f"{__name__}.backend") # registers "docker" with register_backend
20
+
21
+ def configure(self, ctx, cfg):
22
+ return {"backend": "docker"}
23
+
24
+ def status(self) -> dict:
25
+ return {**self.obj, "daemon": docker_available()}
@@ -0,0 +1,123 @@
1
+ """Docker sandbox backend (plugin): throwaway containers for run_code, with toolchain images and offline runs.
2
+ The core keeps the `local` backend and the shared run/venv machinery."""
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ import uuid
8
+ from pathlib import Path, PurePosixPath
9
+
10
+ from xqo.api import (DOCKER_PATH, Backend, Sandbox, current_toolchains, docker_available, docker_kill, docker_user,
11
+ is_python_project, mount_hint, net_args, register_backend, setup_net_args, uv_pip_command)
12
+
13
+ @register_backend
14
+ class DockerBackend(Backend):
15
+ """A throwaway container: no network, memory/CPU/pid limits, the host user's uid. Python projects get a uv venv;
16
+ Node/Go/Rust/.NET projects run in their official image (at the version the project asks for), and mixed projects
17
+ in a composite image built once. Dependencies are installed by setup containers (network on, `setup_network`)
18
+ into the task's .deps-docker dir and shared download caches, which runs mount instead of copying."""
19
+ name = "docker"
20
+ toolchains = True
21
+ isolates_network = True
22
+
23
+ def available(self) -> bool:
24
+ return docker_available()
25
+
26
+ async def command(self, sb, work, command, env_dir, comps, network=False):
27
+ cfg, res, run_cmd = sb.config, {}, command
28
+ image, mounts, env = cfg.image, [], {}
29
+ toolchains = current_toolchains() # the toolchains plugin, or None
30
+ if toolchains and env_dir is not None and cfg.toolchains and any(c.kind != "python" for c in comps):
31
+ plan = toolchains.docker_plan(work, comps, cfg)
32
+ image = plan["image"]
33
+ info = {"kind": "image", "image": image, "tools": plan["tools"]}
34
+ if plan["dockerfile"]:
35
+ built = await sb.build_image(image, plan["dockerfile"], env_dir.parent / ".deps-docker")
36
+ info.update(built)
37
+ res["toolchains"] = [info]
38
+ if not info.get("error"):
39
+ deps = env_dir.parent / ".deps-docker"
40
+ runner = DockerRunner(sb, image, work, deps, toolchains.cache_root(cfg.cache_dir) / "docker")
41
+ prep = await toolchains.prepare(runner, work, comps, deps, cfg, {"HOME": "/tmp"})
42
+ res["toolchains"] += prep.info
43
+ env = {**prep.env, **({} if network or cfg.network else DOCKER_RUN_OFFLINE)}
44
+ mounts = runner.mounts()
45
+ if prep.managed and (note := toolchains.install_note(command)):
46
+ res["notes"] = [note]
47
+ container = f"orch-run-{uuid.uuid4().hex[:12]}" # named, so a cancel can kill it
48
+ argv = ["docker", "run", "--rm", "-i", "--name", container, "--memory", cfg.memory, "--cpus", cfg.cpus,
49
+ "--pids-limit", "256", *docker_user(), "-v", f"{work}:/work", *mounts, "-w", "/work",
50
+ "-e", "HOME=/tmp"]
51
+ argv += [x for k, v in env.items() if k != "HOME" for x in ("-e", f"{k}={v}")]
52
+ if env_dir is not None and cfg.venv and is_python_project(work):
53
+ denv = env_dir.with_name(env_dir.name + "-docker")
54
+ venv = await sb.prepare_docker_env(work, denv, image)
55
+ res["venv"] = venv
56
+ if not venv.get("error"): # run inside the task's venv; uv run/sync use it too
57
+ argv += ["-v", f"{denv}:/venv", "-e", "VIRTUAL_ENV=/venv/env", "-e", "UV_PROJECT_ENVIRONMENT=/venv/env",
58
+ "-e", "UV_CACHE_DIR=/venv/cache", "-e", "UV_PYTHON_INSTALL_DIR=/venv/python",
59
+ "-e", f"PATH=/venv/env/bin:{DOCKER_PATH}"]
60
+ run_cmd = uv_pip_command(command)
61
+ if network: # approved by the user for this run: the host's CA and proxies, like setup containers
62
+ argv += net_args(cfg)
63
+ elif not cfg.network:
64
+ argv += ["--network", "none"]
65
+ argv += [image, "sh", "-c", run_cmd]
66
+ return {"argv": argv, "env": None, "run_cmd": run_cmd, "container": container, "result": res}
67
+
68
+
69
+ # runs have no network: tools fail fast instead of trying to download
70
+ DOCKER_RUN_OFFLINE = {"GOPROXY": "off", "GOTOOLCHAIN": "local", "CARGO_NET_OFFLINE": "true",
71
+ "npm_config_prefer_offline": "true"}
72
+
73
+
74
+ class DockerRunner:
75
+ """toolchains.prepare's setup commands, each in a throwaway container of the run's image: the run copy at /work,
76
+ the task's deps at /deps, the shared download caches at /cache; network on unless setup_network is false."""
77
+ versions = False # the image is the version
78
+ host_env = False
79
+
80
+ def __init__(self, sb: "Sandbox", image: str, work: Path, deps: Path, cache: Path):
81
+ self.sb, self.image, self.work, self.deps, self.cache = sb, image, work, deps, cache
82
+ deps.mkdir(parents=True, exist_ok=True) # made by us, not by Docker (it would make them root's)
83
+ cache.mkdir(parents=True, exist_ok=True)
84
+
85
+ def deps_view(self, deps: Path):
86
+ return PurePosixPath("/deps")
87
+
88
+ def mounts(self) -> list[str]:
89
+ return ["-v", f"{self.deps}:/deps", "-v", f"{self.cache}:/cache"]
90
+
91
+ def where(self, p: Path) -> str:
92
+ """A host path under the run copy or the deps dir, as the container sees it."""
93
+ for host, ctr in ((self.work, "/work"), (self.deps, "/deps")):
94
+ try:
95
+ return (PurePosixPath(ctr) / p.relative_to(host).as_posix()).as_posix()
96
+ except ValueError:
97
+ pass
98
+ raise ValueError(f"{p} is not mounted")
99
+
100
+ def exe(self, argv: list[str], env: dict) -> list[str]:
101
+ return ["corepack", *argv] if argv[0] in ("pnpm", "yarn") else argv # node images ship corepack
102
+
103
+ def link(self, target: Path, link: Path) -> None:
104
+ os.symlink(self.where(target), link, target_is_directory=True) # dangling on the host, valid inside
105
+
106
+ async def sh(self, argv, cwd: Path, env: dict, timeout: float):
107
+ cfg = self.sb.config
108
+ name = f"orch-setup-{uuid.uuid4().hex[:12]}"
109
+ dargv = ["docker", "run", "--rm", "-i", "--name", name, "--memory", cfg.memory, "--cpus", cfg.cpus,
110
+ "--pids-limit", "512", *docker_user(), "-v", f"{self.work}:/work", *self.mounts(),
111
+ *setup_net_args(cfg), "-w", self.where(cwd),
112
+ *[x for k, v in env.items() for x in ("-e", f"{k}={v}")]]
113
+ if not cfg.setup_network:
114
+ dargv += ["--network", "none"]
115
+ try:
116
+ code, out = await self.sb._sh([*dargv, self.image, *argv], self.work, dict(os.environ), timeout)
117
+ except asyncio.CancelledError:
118
+ await docker_kill(name)
119
+ raise
120
+ if code == -1:
121
+ await docker_kill(name)
122
+ return code, (mount_hint(out) + "\n" + out) if code == 125 and mount_hint(out) else out
123
+
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: xqo-docker
3
+ Version: 3.0.0
4
+ Summary: Docker sandbox backend for xqo run_code
5
+ Author: dudikkocak
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/dudikkocak/xqo
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: xqo<4,>=3.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=8; extra == "dev"
14
+ Requires-Dist: pytest-timeout>=2.3; extra == "dev"
15
+ Dynamic: license-file
16
+
17
+ # xqo-docker
18
+
19
+ The `docker` sandbox backend for [xqo](https://github.com/dudikkocak/xqo)'s `run_code`: each run happens in a
20
+ throwaway container with no network (unless the user grants it), memory, CPU and process limits, and the host
21
+ user's uid. Python projects get a uv venv inside the image; Node, Go, Rust and .NET projects use their official
22
+ images when the toolchains plugin is installed.
23
+
24
+ ```bash
25
+ pip install xqo-docker
26
+ ```
27
+
28
+ ```json
29
+ "sandbox": {"backend": "docker"}
30
+ ```
31
+
32
+ With the plugin installed, `"backend": "auto"` picks Docker whenever the daemon answers and falls back to `local`
33
+ otherwise. `"plugins": {"disable": ["docker"]}` turns it off without uninstalling. `/api/plugins` shows whether the
34
+ daemon answered (`status.daemon`).
35
+
36
+ Needs the `docker` CLI and a running daemon (Docker Desktop, Docker Engine or a compatible one). The image is set
37
+ with `sandbox.image` (default `astral/uv:python3.12-bookworm-slim`); it needs `uv` and `python` on its PATH.
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/xqo_docker/__init__.py
5
+ src/xqo_docker/backend.py
6
+ src/xqo_docker.egg-info/PKG-INFO
7
+ src/xqo_docker.egg-info/SOURCES.txt
8
+ src/xqo_docker.egg-info/dependency_links.txt
9
+ src/xqo_docker.egg-info/entry_points.txt
10
+ src/xqo_docker.egg-info/requires.txt
11
+ src/xqo_docker.egg-info/top_level.txt
12
+ tests/test_docker.py
@@ -0,0 +1,2 @@
1
+ [xqo.plugins]
2
+ docker = xqo_docker:DockerPlugin
@@ -0,0 +1,5 @@
1
+ xqo<4,>=3.0
2
+
3
+ [dev]
4
+ pytest>=8
5
+ pytest-timeout>=2.3
@@ -0,0 +1 @@
1
+ xqo_docker
@@ -0,0 +1,126 @@
1
+ """The docker sandbox backend, driven by a fake `docker` script (no daemon needed), plus one real-daemon run."""
2
+ import asyncio
3
+ import json
4
+ import os
5
+ import subprocess
6
+
7
+ import pytest
8
+
9
+ from xqo.api import Sandbox, SandboxConfig, build, docker_available, mount_hint
10
+ from xqo.sandbox import format_result # not part of the plugin API; used here only to check the report text
11
+
12
+ posix_only = pytest.mark.skipif(os.name == "nt", reason="the fake docker is a sh script")
13
+ PREFIX = "python -c 'import sys; print(sys.prefix)'"
14
+ DENIED = ("docker: Error response from daemon: mounts denied:\nThe path /tmp/orch-run-cgm9yxvr/work is not shared "
15
+ "from the host and is not known to Docker.\nYou can configure shared paths from Docker -> Preferences...\n")
16
+
17
+
18
+ def fake_docker(tmp_path, monkeypatch, script):
19
+ bin_ = tmp_path / "bin"
20
+ bin_.mkdir()
21
+ (bin_ / "docker").write_text("#!/bin/sh\n" + script)
22
+ (bin_ / "docker").chmod(0o755)
23
+ monkeypatch.setenv("PATH", f"{bin_}{os.pathsep}{os.environ['PATH']}")
24
+
25
+
26
+ def project(tmp_path, reqs="# no deps\n"):
27
+ ws = tmp_path / "ws"
28
+ ws.mkdir(parents=True)
29
+ (ws / "m.py").write_text("print('ok')\n")
30
+ (ws / "requirements.txt").write_text(reqs)
31
+ return ws
32
+
33
+
34
+ def test_plugin_is_found_and_active(tmp_path):
35
+ cfg = tmp_path / "agents.json"
36
+ cfg.write_text(json.dumps({"agents": [{"name": "planner", "skills": ["planning"]}]}))
37
+ bus = build(cfg, tmp_path / "artifacts", str(tmp_path / "logs" / "events.jsonl"), mock=True, departments="none")
38
+ st = bus.plugins.states["docker"]
39
+ assert st.state == "active" and st.source == "xqo_docker:DockerPlugin"
40
+ assert Sandbox(SandboxConfig(backend="docker")).network_requestable
41
+
42
+
43
+ @posix_only
44
+ def test_run_with_network_drops_none_and_offline_env(tmp_path, monkeypatch):
45
+ log = tmp_path / "docker.log"
46
+ fake_docker(tmp_path, monkeypatch, f'printf "%s\\n" "$@" > "{log}"\n')
47
+ ca = tmp_path / "ca.pem"
48
+ ca.write_text("x")
49
+ sb = Sandbox(SandboxConfig(backend="docker", venv=False, ca_file=str(ca)))
50
+ asyncio.run(sb.run(tmp_path / "ws", "curl https://example.com", network=True))
51
+ argv = log.read_text().splitlines()
52
+ assert "none" not in argv and f"{ca.resolve()}:/etc/ssl/orch-ca.pem:ro" in argv
53
+ asyncio.run(sb.run(tmp_path / "ws", "curl https://example.com"))
54
+ argv = log.read_text().splitlines()
55
+ assert argv[argv.index("--network") + 1] == "none" and not any("orch-ca" in a for a in argv)
56
+
57
+
58
+ @posix_only
59
+ def test_mounts_the_run_dir_copy(tmp_path, monkeypatch):
60
+ log = tmp_path / "docker.log"
61
+ fake_docker(tmp_path, monkeypatch, f'printf "%s\\n" "$@" > "{log}"\n')
62
+ ws = tmp_path / "ws"
63
+ ws.mkdir()
64
+ root = tmp_path / "runs"
65
+ r = asyncio.run(Sandbox(SandboxConfig(backend="docker", run_dir=str(root))).run(ws, "true"))
66
+ mount = next(a for a in log.read_text().splitlines() if a.endswith(":/work"))
67
+ assert mount.startswith(str(root.resolve()) + os.sep + "orch-run-") and r["exit_code"] == 0 and "error" not in r
68
+ assert not list(root.glob("orch-run-*")) # removed after the run
69
+
70
+
71
+ @posix_only
72
+ def test_mounts_denied_reported_with_a_fix(tmp_path, monkeypatch):
73
+ assert "File Sharing" in mount_hint(DENIED)
74
+ fake_docker(tmp_path, monkeypatch, f"cat >&2 <<'X'\n{DENIED}X\nexit 125\n")
75
+ ws = tmp_path / "ws"
76
+ ws.mkdir()
77
+ (ws / "requirements.txt").write_text("# none\n")
78
+ sb = Sandbox(SandboxConfig(backend="docker", run_dir=str(tmp_path / "runs")))
79
+ r = asyncio.run(sb.run(ws, "python -m unittest", env_dir=tmp_path / "task" / ".venv"))
80
+ assert r["exit_code"] == 125 and "File Sharing" in r["error"] and "File Sharing" in r["venv"]["error"]
81
+ out = format_result(r)
82
+ assert "[sandbox ERROR: Docker can't mount" in out and "[venv ERROR: Docker can't mount" in out
83
+
84
+
85
+ FAKE_DOCKER = """printf '%s\\0' "$@" >> "$XQO_FAKE_DOCKER_LOG"; printf '\\036' >> "$XQO_FAKE_DOCKER_LOG"\n"""
86
+
87
+
88
+ @posix_only
89
+ def test_venv_made_with_uv_in_the_image(tmp_path, monkeypatch):
90
+ fake_docker(tmp_path, monkeypatch, FAKE_DOCKER)
91
+ log = tmp_path / "docker.log"
92
+ monkeypatch.setenv("XQO_FAKE_DOCKER_LOG", str(log))
93
+ ws, env = project(tmp_path), tmp_path / "task" / ".venv"
94
+ (ws / "tests").mkdir()
95
+ sb = Sandbox(SandboxConfig(backend="docker"))
96
+ r = asyncio.run(sb.run(ws, "pytest -q", env_dir=env))
97
+ calls = lambda: [c.split("\0")[:-1] for c in log.read_text().split("\x1e")[:-1]] # noqa: E731
98
+ setup, run = calls()
99
+ script = setup[-1]
100
+ assert f"{env}-docker:/venv" in setup and setup[:2] == ["run", "--rm"]
101
+ assert "uv venv --quiet /venv/env" in script and "uv pip install --quiet --python /venv/env/bin/python -r requirements.txt" in script
102
+ assert "pytest" in script and "-m venv" not in script and "\npip " not in script
103
+ assert SandboxConfig().image in setup and "--network" not in setup # deps may be downloaded during setup
104
+ assert f"{env}-docker:/venv" in run and "VIRTUAL_ENV=/venv/env" in run and run[run.index("none") - 1] == "--network"
105
+ assert r["venv"]["tool"] == "uv" and r["venv"]["created"] and r["venv"]["installed"] == ["requirements.txt"]
106
+ (tmp_path / "task" / ".venv-docker" / "env").mkdir() # what the image's uv would have made
107
+ r = asyncio.run(sb.run(ws, "pytest -q", env_dir=env)) # same deps: no setup container
108
+ assert r["venv"].get("reused") and len(calls()) == 3
109
+ r = asyncio.run(sb.run(ws, "pip install x", env_dir=env))
110
+ assert calls()[-1][-1] == "uv pip install x" and r["ran"] == calls()[-1][-1] and "VIRTUAL_ENV=/venv/env" in calls()[-1]
111
+
112
+
113
+ IMAGE = SandboxConfig().image
114
+
115
+
116
+ def _image_ready():
117
+ return docker_available() and subprocess.run(["docker", "image", "inspect", IMAGE], capture_output=True).returncode == 0
118
+
119
+
120
+ @pytest.mark.skipif(not _image_ready(), reason=f"docker or the {IMAGE} image is not available")
121
+ def test_real_docker_runs_in_uv_venv(tmp_path):
122
+ ws, env = project(tmp_path), tmp_path / "task" / ".venv"
123
+ r = asyncio.run(Sandbox(SandboxConfig(backend="docker", timeout=60)).run(ws, PREFIX + " && uv --version", env_dir=env))
124
+ assert r["exit_code"] == 0, r
125
+ assert r["stdout"].splitlines()[0] == "/venv/env" and r["stdout"].splitlines()[1].startswith("uv ")
126
+ assert (tmp_path / "task" / ".venv-docker" / "env").is_dir()