From 200a501f15a24a4152f92897a6b72af238ee69ef Mon Sep 17 00:00:00 2001 From: Ahmed Allam Date: Sat, 24 Oct 2026 21:34:33 +0000 Subject: [PATCH] feat(runtime): pass host devices from STRIX_DOCKER_SANDBOX_DEVICES through to the sandbox container --- strix/runtime/docker_client.py | 28 +++++++++++++++++++++++++ tests/test_docker_client_devices.py | 32 +++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 tests/test_docker_client_devices.py diff --git a/strix/runtime/docker_client.py b/strix/runtime/docker_client.py index 969267d53..68ffa2fba 100644 --- a/strix/runtime/docker_client.py +++ b/strix/runtime/docker_client.py @@ -66,6 +66,33 @@ def _apply_sandbox_network(create_kwargs: dict[str, Any]) -> None: create_kwargs.pop("ports", None) +_SANDBOX_DEVICES_ENV = "STRIX_DOCKER_SANDBOX_DEVICES" + + +def _sandbox_devices() -> list[str]: + raw = os.environ.get(_SANDBOX_DEVICES_ENV, "") + return [item.strip() for item in raw.split(",") if item.strip()] + + +def _apply_sandbox_devices(create_kwargs: dict[str, Any]) -> None: + """Pass the host devices listed in ``STRIX_DOCKER_SANDBOX_DEVICES`` through. + + Comma-separated device paths, each mapped to the same path in the container + with ``rwm`` access. A tunnel brought up inside the sandbox (OpenVPN, + Tailscale) needs ``/dev/net/tun``: Docker's default device cgroup denies it + even with ``NET_ADMIN``. Unset means no extra devices. + """ + devices = _sandbox_devices() + if not devices: + return + existing = [str(item) for item in (create_kwargs.get("devices") or [])] + for path in devices: + spec = f"{path}:{path}:rwm" + if spec not in existing and path not in existing: + existing.append(spec) + create_kwargs["devices"] = existing + + def _apply_resource_limits(create_kwargs: dict[str, Any]) -> None: """Apply optional cgroup resource caps from the environment. Unset/blank values leave docker's default (unbounded), so this is opt-in per host.""" @@ -233,6 +260,7 @@ class StrixDockerSandboxClient(DockerSandboxClient): extra_hosts["host.docker.internal"] = "host-gateway" _apply_sandbox_network(create_kwargs) + _apply_sandbox_devices(create_kwargs) _apply_resource_limits(create_kwargs) _apply_log_limits(create_kwargs) _apply_run_labels(create_kwargs) diff --git a/tests/test_docker_client_devices.py b/tests/test_docker_client_devices.py new file mode 100644 index 000000000..54e15f000 --- /dev/null +++ b/tests/test_docker_client_devices.py @@ -0,0 +1,32 @@ +"""``STRIX_DOCKER_SANDBOX_DEVICES`` passes host devices through to the sandbox container.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from strix.runtime import docker_client + + +if TYPE_CHECKING: + import pytest + + +def test_unset_adds_no_devices(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv(docker_client._SANDBOX_DEVICES_ENV, raising=False) + kwargs: dict[str, object] = {} + docker_client._apply_sandbox_devices(kwargs) + assert kwargs == {} + + +def test_devices_are_mapped_to_the_same_path_rwm(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv(docker_client._SANDBOX_DEVICES_ENV, " /dev/net/tun, /dev/kvm ,,") + kwargs: dict[str, object] = {} + docker_client._apply_sandbox_devices(kwargs) + assert kwargs["devices"] == ["/dev/net/tun:/dev/net/tun:rwm", "/dev/kvm:/dev/kvm:rwm"] + + +def test_existing_devices_are_kept_and_not_duplicated(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv(docker_client._SANDBOX_DEVICES_ENV, "/dev/fuse,/dev/net/tun") + kwargs: dict[str, object] = {"devices": ["/dev/fuse"]} + docker_client._apply_sandbox_devices(kwargs) + assert kwargs["devices"] == ["/dev/fuse", "/dev/net/tun:/dev/net/tun:rwm"]