Docker tasks: --docker-runtime option (runsc support, default off)
DockerExecutor gains a runtime parameter (passed as docker --runtime in build_run_command, after --pids-limit); worker CLI gains --docker-runtime, wired through _build_cli_executor. Unset or empty means no --runtime flag, so existing tasks keep docker default (runc); an unregistered runtime fails container start (fail closed), it never silently falls back. Tests: runsc appears in the command, default/empty omit it, CLI wiring passes through. README documents the flag and corrects a stale multi-tenant note (host kernel does not ban userns; bwrap fails inside Docker containers on nested-pidns proc mount).
This commit is contained in:
1 parent
b31e9365de
commit
9f05178f2b
4 files changed
+93
-5
No files matched your search
+6
-2
@@ -489,14 +489,18 @@ interrupted 且终态任务 cancel 返回 400、心跳后 sweep 不扫刚心跳
|
||||
`--cap-drop=ALL --security-opt=no-new-privileges`。codex-home
|
||||
只播种模板的 config.toml(relay 端点与 provider 名,不含
|
||||
密钥值),绝不复制 thread_history。注意:内层 codex 沙箱仍
|
||||
是 danger-full-access(主机内核禁 user namespace,bwrap 不可
|
||||
用),隔离边界是容器 + 挂载 + 网络三层,不是内层沙箱。
|
||||
是 danger-full-access(bwrap 在 Docker 容器内不可用:嵌套
|
||||
pidns 下 fresh proc mount 内核 EPERM,宿主级则可用,见下),
|
||||
隔离边界是容器 + 挂载 + 网络三层,不是内层沙箱。
|
||||
- 外层加固(内层保持 danger-full-access 时的补偿层):
|
||||
`--docker-security-opt`(可重复,如 `apparmor=docker-codex`,
|
||||
见 `deploy/apparmor/docker-codex`:在 Docker 默认 profile
|
||||
基础上禁 raw 网络、ptrace、mount 与危险 capability;注意
|
||||
它不含 `userns`,即不启用 bwrap 内层沙箱路线);
|
||||
`--docker-pids-limit`(默认 512,防 fork 炸弹);
|
||||
`--docker-runtime`(不传 = docker 默认 runc;传 `runsc` =
|
||||
任务容器走 gVisor syscall 沙箱,需 daemon 已注册该 runtime,
|
||||
否则起容器失败;生产切换前须经一次性 worker 模型任务验证);
|
||||
`--egress-proxy-port`(0 = 不启用;生产配 8888):启用后
|
||||
Docker 任务容器的 HTTP(S) 出口全部走宿主上的 Tinyproxy
|
||||
(见 `deploy/egress/`:FilterDefaultDeny 白名单,只放行
|
||||
|
||||
+11
-2
@@ -265,7 +265,7 @@ class DockerExecutor(Executor):
|
||||
user="1000:1000", worktrees_root=None, env=None,
|
||||
env_key="XCM_API_KEY", timeout=None, extra_mounts=None,
|
||||
model=None, security_opts=None, pids_limit=None,
|
||||
extra_env=None):
|
||||
runtime=None, extra_env=None):
|
||||
if host_py_path is None:
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
host_py_path = os.path.join(os.path.dirname(here), "host.py")
|
||||
@@ -294,6 +294,13 @@ class DockerExecutor(Executor):
|
||||
# 的 -e 名方式)。
|
||||
self.security_opts = list(security_opts or [])
|
||||
self.pids_limit = pids_limit
|
||||
# runtime:docker --runtime(如 runsc,gVisor 沙箱)。
|
||||
# None 或空串 = 不传 --runtime,走 docker 默认(runc),
|
||||
# 现有任务行为不变;显式值由 docker 在起容器时校验,
|
||||
# 未注册的 runtime 会起容器失败(fail closed)。
|
||||
self.runtime = runtime.strip() if isinstance(runtime, str) else runtime
|
||||
if not self.runtime:
|
||||
self.runtime = None
|
||||
self.extra_env = dict(extra_env or {})
|
||||
|
||||
@staticmethod
|
||||
@@ -387,7 +394,7 @@ class DockerExecutor(Executor):
|
||||
顺序固定:docker run --rm --name <容器名> --user <用户>
|
||||
--cpus <核数> --memory <内存> [--cap-drop=ALL
|
||||
--security-opt=no-new-privileges] [--security-opt <自定义>]
|
||||
[--pids-limit <n>] [--network <网络>]
|
||||
[--pids-limit <n>] [--runtime <runtime>] [--network <网络>]
|
||||
-v <worktree>:/work -v <代码目录>:/app:ro -w /work
|
||||
[-e <env_key名>] [-e <NAME=VALUE>...] <镜像> <容器内命令...>。
|
||||
-e 只传名字,不带 =值(env_key);extra_env 的 -e 带值,
|
||||
@@ -415,6 +422,8 @@ class DockerExecutor(Executor):
|
||||
cmd += ["--security-opt", opt]
|
||||
if self.pids_limit is not None:
|
||||
cmd += ["--pids-limit", str(self.pids_limit)]
|
||||
if self.runtime:
|
||||
cmd += ["--runtime", self.runtime]
|
||||
if network:
|
||||
cmd += ["--network", network]
|
||||
cmd += ["-v", "%s:/work" % self._worktree_for(task),
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Docker runtime 单测:--runtime=runsc 接入。
|
||||
|
||||
只用假 runner,不跑真实 Docker。覆盖:显式 runtime 进命令
|
||||
(且在 --pids-limit 之后)、默认/空串不传 --runtime、
|
||||
worker CLI --docker-runtime 透传到 DockerExecutor。
|
||||
"""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
try:
|
||||
from cloud.sandbox import DockerExecutor
|
||||
from cloud.worker import _build_cli_executor
|
||||
except ImportError:
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from cloud.sandbox import DockerExecutor
|
||||
from cloud.worker import _build_cli_executor
|
||||
|
||||
|
||||
def _executor(**kwargs):
|
||||
def fake(cmd, timeout=None):
|
||||
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
||||
kwargs.setdefault("env", {"XCM_API_KEY": "test-placeholder"})
|
||||
kwargs.setdefault("runner", fake)
|
||||
with mock.patch("cloud.sandbox.shutil.which",
|
||||
return_value="/usr/bin/docker"):
|
||||
return DockerExecutor("/fake/host.py", **kwargs)
|
||||
|
||||
|
||||
def _cmd(ex):
|
||||
with mock.patch("cloud.sandbox.shutil.which",
|
||||
return_value="/usr/bin/docker"):
|
||||
return ex.build_run_command(
|
||||
{"id": "t1", "worktree": "/tmp/wt"}, ["echo", "hi"])
|
||||
|
||||
|
||||
class DockerRuntimeTestCase(unittest.TestCase):
|
||||
def test_runsc_appears_in_command(self):
|
||||
cmd = _cmd(_executor(runtime="runsc", pids_limit=512))
|
||||
self.assertIn("--runtime", cmd)
|
||||
self.assertEqual(cmd[cmd.index("--runtime") + 1], "runsc")
|
||||
# 顺序:--runtime 在 --pids-limit 之后。
|
||||
self.assertLess(cmd.index("--pids-limit"), cmd.index("--runtime"))
|
||||
|
||||
def test_default_and_empty_omit_runtime(self):
|
||||
self.assertNotIn("--runtime", _cmd(_executor()))
|
||||
self.assertNotIn("--runtime", _cmd(_executor(runtime="")))
|
||||
self.assertNotIn("--runtime", _cmd(_executor(runtime=" ")))
|
||||
|
||||
def test_cli_wiring(self):
|
||||
args = SimpleNamespace(
|
||||
executor="docker", config=None,
|
||||
docker_image="codex-app-server:latest",
|
||||
docker_cpus="2", docker_memory="2g", docker_user="1000:1000",
|
||||
docker_worktrees_root=None, env_key=None, codex_config=None,
|
||||
docker_timeout=None, docker_extra_mounts=None,
|
||||
docker_security_opt=None, docker_pids_limit=512,
|
||||
docker_runtime="runsc")
|
||||
ex = _build_cli_executor(args, "/fake/host.py")
|
||||
self.assertIsInstance(ex, DockerExecutor)
|
||||
self.assertEqual(ex.runtime, "runsc")
|
||||
args2 = SimpleNamespace(**{**vars(args), "docker_runtime": None})
|
||||
self.assertIsNone(_build_cli_executor(args2, "/fake/host.py").runtime)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+6
-1
@@ -1792,7 +1792,8 @@ def _build_cli_executor(args, host_py):
|
||||
timeout=args.docker_timeout,
|
||||
extra_mounts=args.docker_extra_mounts,
|
||||
security_opts=args.docker_security_opt,
|
||||
pids_limit=args.docker_pids_limit)
|
||||
pids_limit=args.docker_pids_limit,
|
||||
runtime=getattr(args, "docker_runtime", None))
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
@@ -1877,6 +1878,10 @@ def main(argv=None):
|
||||
parser.add_argument("--docker-pids-limit", type=int, default=512,
|
||||
help="docker --pids-limit(默认 512,防 fork 炸弹;"
|
||||
"codex 工具链正常用量远低于此)")
|
||||
parser.add_argument("--docker-runtime", default=None,
|
||||
help="docker --runtime(不传 = docker 默认 runc;"
|
||||
"传 runsc = 任务容器走 gVisor 沙箱,需 daemon"
|
||||
"已注册该 runtime,否则起容器失败)")
|
||||
parser.add_argument("--egress-proxy-port", type=int, default=0,
|
||||
help="egress 过滤代理端口(0 = 不启用;生产配"
|
||||
" 8888):启用后 Docker 任务容器的 HTTP(S)"
|
||||
|
||||
Reference in new issue
Block a user