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 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [xqo.plugins]
2
+ docker = xqo_docker:DockerPlugin
@@ -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