xqo-docker 3.0.0__py3-none-any.whl
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.
- xqo_docker/__init__.py +25 -0
- xqo_docker/backend.py +123 -0
- xqo_docker-3.0.0.dist-info/METADATA +37 -0
- xqo_docker-3.0.0.dist-info/RECORD +8 -0
- xqo_docker-3.0.0.dist-info/WHEEL +5 -0
- xqo_docker-3.0.0.dist-info/entry_points.txt +2 -0
- xqo_docker-3.0.0.dist-info/licenses/LICENSE +21 -0
- xqo_docker-3.0.0.dist-info/top_level.txt +1 -0
xqo_docker/__init__.py
ADDED
|
@@ -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()}
|
xqo_docker/backend.py
ADDED
|
@@ -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,8 @@
|
|
|
1
|
+
xqo_docker/__init__.py,sha256=kWTgCrG4eSonCvYgEkpDTUCV6xK0AQPVmf2tW-YnBWU,839
|
|
2
|
+
xqo_docker/backend.py,sha256=8GATXfkdAGv3WqwT-4S7NznpZlvucn4Vmk5AOmcMCxI,6716
|
|
3
|
+
xqo_docker-3.0.0.dist-info/licenses/LICENSE,sha256=PHtf4TI2P6oulwhOKBF8SgeOkDLp9YnLdq46mw6506s,1067
|
|
4
|
+
xqo_docker-3.0.0.dist-info/METADATA,sha256=BaSG7P6YWcOiZTC7R8qWYSSvP0CAHvz2EYN6DyAiPzI,1417
|
|
5
|
+
xqo_docker-3.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
6
|
+
xqo_docker-3.0.0.dist-info/entry_points.txt,sha256=zzjBST_E3BZZOzSKKl8Br36CQBX6ELvxtxVkhfmCgRM,47
|
|
7
|
+
xqo_docker-3.0.0.dist-info/top_level.txt,sha256=w3urj2YYpNGAHWZ5UdpVrfmD7BI3dl09Zui_GjKmisc,11
|
|
8
|
+
xqo_docker-3.0.0.dist-info/RECORD,,
|
|
@@ -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 @@
|
|
|
1
|
+
xqo_docker
|