代码沙箱
在隔离的 Bubblewrap 命名空间中安全运行 Python 与 JavaScript 代码
代码沙箱是 Clouisle 的安全隔离执行环境,用于在工作流、Agent 和工具中运行用户提供的 Python 与 JavaScript 代码。沙箱任务进入专用 Celery Worker(独立消费 sandbox 队列,与主 Worker 的四个队列分开),每个任务在独立的 Bubblewrap 用户+挂载命名空间中执行,并由超时、输出限制与磁盘检查约束失控任务。
官方部署的 sandbox-worker 以 root(user: "0" / runAsUser: 0)运行,并在运行时默认 cap 集之上叠加 CAP_SYS_ADMIN,同时使用 seccomp=unconfined(保留 no-new-privileges)——它不是 rootless 容器。隔离来自每个任务的 Bubblewrap 用户+挂载命名空间,而不是 Worker 自身被降权;逃逸后果与加固方式见下文「加固:收敛 Worker 的 CAP_SYS_ADMIN」。
隔离架构与 Bubblewrap 挂载布局
Agent/工作流 → API → Celery 队列 (sandbox) → 沙箱 Worker
↓
Bubblewrap 进程
↓
/workspace → 当前任务/会话目录当开启文件系统隔离(SANDBOX_FILESYSTEM_ISOLATION_ENABLED=true)时,沙箱启动器将命令包裹在 bwrap 进程中执行:
bwrap \
--unshare-user --unshare-pid --unshare-ipc --unshare-uts \
--die-with-parent --hostname clouisle-sandbox \
--ro-bind /usr /usr --ro-bind /bin /bin --ro-bind /lib /lib --ro-bind /etc /etc \
--dir /proc --dev /dev \
--bind <host_job_dir> /workspace \
--bind <host_job_dir>/tmp /tmp \
--chdir /workspace \
-- <command> <args...>- 命名空间与生命周期隔离:
- 多重隔离:
--unshare-user、--unshare-pid、--unshare-ipc、--unshare-uts创建独立的用户、进程树(映射为 PID 1)、IPC 通信域与主机名(clouisle-sandbox)。 - 进程级联回收:
--die-with-parent保证 Worker 主管进程退出或超时后,沙箱内所有衍生子进程被内核立即回收。
- 多重隔离:
- 只读系统根与工作目录映射 (
RUNTIME_READONLY_ROOTS):- 系统目录只读:
/usr、/bin、/sbin、/lib、/lib64、/etc通过--ro-bind只读挂载,防止修改系统二进制、Python site-packages 或 OS 配置。 - 独立工作空间:宿主任务目录映射为标准的
/workspace,内部配备独立的/tmp子挂载(--bind <workspace>/tmp /tmp),隔离临时文件。 - 逻辑路径自动重写:
_logical_workspace_path与_map_workspace_value自动将任务参数和环境变量中的物理路径重写为/workspace/...。
- 系统目录只读:
- 运行时与中日韩(CJK)字体支持:
- 预置运行时:预装 Python 3.13(基于
uv管理)与 Node.js 22。 - CJK 字体支持:
clouisle-sandbox-worker镜像预装fontconfig、fonts-wqy-zenhei与fonts-wqy-microhei并刷新字体缓存;/usr/share/fonts通过只读挂载暴露给沙箱,使用matplotlib、seaborn、Pillow生成图表时可正常渲染中日韩字符,杜绝方块乱码(豆腐块)。
- 预置运行时:预装 Python 3.13(基于
支持的运行时
| 运行时 | 基础环境 |
|---|---|
| Python | Python 3.13,包含标准库和任务配置的依赖 |
| JavaScript | Node.js 22,包含核心模块和任务配置的依赖 |
使用场景
- 代码工具:在 管理后台 > 功能 > 代码 中创建可复用的代码工具,可安装精确固定版本的包并声明
/workspace下的产物,保存后被 Agent 和工作流调用(见 自定义 HTTP 与代码工具)。 - 工作流代码节点:在工作流图中直接嵌入代码,接收输入变量并将结果返回给下游节点(见 工作流节点)。
- Agent 级执行:Agent 通过函数调用触发代码工具,由 LLM 根据任务需要决定何时运行代码。
执行边界
沙箱任务限制由 SandboxLimits 统一定义,实际生效值取「任务配置」与「调用侧超时」中先到的一个:
| 限制 | 默认值 | 允许范围 | 说明 |
|---|---|---|---|
timeout_seconds | 30 | 1-600 | 单次任务的执行超时(秒);超时后按进程组 SIGTERM → SIGKILL 回收 |
disk_mb | 1024 | 0-102400(并受 SANDBOX_MAX_DISK_MB 默认 8192 约束) | 工作空间磁盘配额 |
max_stdout_kb | 256 | 16-8192 | 标准输出上限,超出部分截断并追加 ...<truncated> |
max_stderr_kb | 256 | 16-8192 | 标准错误上限,同上 |
调用侧还有一层独立的超时:直接执行接口 POST /api/v1/tools/execute-code 的 timeout 默认 30、范围 1-60;Agent 对话中调用代码工具时,外层上限来自 STREAM_TOOL_TIMEOUT_CODE(默认 60 秒)。已保存工具的 limits.timeout_seconds 优先于调用侧传入值(只在配置缺省时才用传入值补位),两边先触发的一方决定结果:沙箱内超时会让任务以执行超时失败,外层超时则表现为工具调用超时。
产物路径必须位于 /workspace 内,单产物默认上限 10MB、产物总量默认上限 10MB(SANDBOX_ARTIFACT_MAX_FILE_SIZE_MB / SANDBOX_ARTIFACT_MAX_TOTAL_SIZE_MB)。边界明细见 工具与 Skills 的沙箱边界。
安全模型
- 任务负载在全新的 Bubblewrap 用户+挂载命名空间内执行,绝不会运行在 Worker 自身的命名空间里。
- 项目提供的部署让 Sandbox Worker 以 root 运行,并在运行时默认 cap 集上叠加
CAP_SYS_ADMIN:镜像的非 root 用户 effective capabilities 恒为空,而特权 Worker 即使在宿主禁止非特权用户命名空间时也能创建用户命名空间。Worker 保持allowPrivilegeEscalation=false,且仅对 sandbox-worker 使用seccomp=unconfined。 - 禁止
seccomp=unconfined的集群需要提供允许 namespace/mount 系统调用的 Localhost seccomp profile。 - 任务命名空间内只有当前工作空间及其临时目录可写;依赖缓存和必要运行时目录只读挂载。
- 子进程只接收过滤后的环境变量,而不是 Worker 的完整进程环境。
- 会话过期后自动清理工作目录。
宿主内核要求
Bubblewrap 通过 unshare(CLONE_NEWUSER) 创建新的用户命名空间。项目提供的部署(Docker Compose、Helm、Kubernetes)让 Worker 以 root + CAP_SYS_ADMIN 运行,用户命名空间创建走特权路径,即使宿主限制非特权用户命名空间也能正常工作——无需修改宿主 sysctl。
自定义部署若保持 Worker 非 root,则依赖宿主内核允许非特权用户命名空间,否则所有沙箱任务都会失败,报错为:
bwrap: No permissions to create new namespace, likely because the kernel does not allow non-privileged user namespaces.部分常见宿主发行版默认限制该能力,且 seccomp=unconfined 无法解决——限制发生在容器 seccomp profile 更底层的宿主内核:
| 发行版 | 限制 | 修复 |
|---|---|---|
| Ubuntu 23.10+ | AppArmor 禁止非特权进程创建用户命名空间(kernel.apparmor_restrict_unprivileged_userns=1) | sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 |
| Debian / 旧内核 | 用户命名空间克隆被禁用(kernel.unprivileged_userns_clone=0) | sysctl -w kernel.unprivileged_userns_clone=1 |
检查当前状态并验证用户命名空间确实可创建:
sysctl kernel.apparmor_restrict_unprivileged_userns kernel.unprivileged_userns_clone 2>/dev/null
unshare -U true && echo "user namespaces OK"使修改持久化:
echo 'kernel.apparmor_restrict_unprivileged_userns=0' > /etc/sysctl.d/99-clouisle-userns.conf
sysctl --system注意事项:
- sysctl 是宿主/节点级设置。在 Kubernetes 中无法按 Pod 设置:需要对每个节点生效(自管节点写入
/etc/sysctl.d/,托管集群使用自定义节点镜像或等效的节点初始化配置)。 - 允许非特权用户命名空间是 rootless 容器(Bubblewrap、Flatpak、Podman)的标准前提。
加固:收敛 Worker 的 CAP_SYS_ADMIN
沙箱任务运行在全新的 Bubblewrap 用户+挂载命名空间内,无法直接触及 Worker 容器的 capabilities。但如果任务成功逃出 Bubblewrap,落点就是容器内 root + CAP_SYS_ADMIN。默认 Docker daemon 下容器与宿主共享初始用户命名空间,该 capability 是宿主 userns 级别的,经典逃逸链(cgroup release_agent、重挂 /proc 写入 kernel.core_pattern、sysctl 写入)在原理上可达。
- Docker Compose:启用 daemon 用户命名空间重映射(
/etc/docker/daemon.json中"userns-remap": "default"),让每个容器进入嵌套用户命名空间——CAP_SYS_ADMIN只作用于容器自身的 userns,宿主逃逸链全部失效。沙箱 Worker 仍能在重映射后的命名空间内特权创建自己的 Bubblewrap userns,沙箱功能不受影响。启用后需重设既有命名卷属主,且 daemon 上所有容器都会被重映射。 - Kubernetes:不适用 daemon 级重映射;依赖 NetworkPolicy 限制沙箱出网、及时升级 Bubblewrap 并关注其 CVE,或在集群提供节点级用户命名空间支持时使用该能力。
运行时开关与降级行为
沙箱链路上有四个关键开关,默认配置更偏向「代码总能跑起来」而不是「隔离一定生效」,生产部署需要逐项确认:
| 环境变量 | 默认值 | 作用 |
|---|---|---|
SANDBOX_RUNTIME_ENABLED | true | 设为 false 时完全不走沙箱网关,代码直接在 API/Worker 进程内执行(无任何隔离),仅供本地调试 |
SANDBOX_LEGACY_FALLBACK_ENABLED | true | 沙箱任务失败或网关抛异常时,回退到进程内的 legacy runner |
SANDBOX_FILESYSTEM_ISOLATION_ENABLED | 代码 false;镜像/Compose/Helm/K8s true | 把可执行载荷包进 Bubblewrap 挂载命名空间 |
SANDBOX_FILESYSTEM_ISOLATION_BINARY | 代码 bwrap;部署 /usr/bin/bwrap | Bubblewrap 可执行文件名或绝对路径 |
为什么 SANDBOX_LEGACY_FALLBACK_ENABLED 值得单独关注:execute_code 这条调用路径在沙箱网关返回失败或抛出异常时,会静默改用进程内 legacy runner 重新执行,只在服务端日志留一条 warning。也就是说,沙箱 Worker 宕机、bwrap 缺失、队列无人消费这些情况,默认结果不是「代码被拒绝执行」,而是「代码退回到没有隔离的进程内执行」——隔离保护在无声中消失,而调用方拿到的是一个「成功」的结果。
要 fail-closed,把它设为 false:沙箱失败会直接把失败结果返回给调用方,不再降级。安全敏感的部署应当关闭该开关,并对沙箱任务失败率设告警。
反方向的坑同样要提防:文件系统隔离已启用时,找不到 bwrap、工作空间根目录不存在、或任务 cwd 逃出工作空间,启动器会抛 SandboxIsolationError 让任务失败——启动器自身不会退化成未隔离执行。但上层 SANDBOX_LEGACY_FALLBACK_ENABLED=true 仍会捕获这次失败并降级,所以只配置隔离开关、不关闭 fallback,并不能保证隔离一定生效。
相关配置
沙箱环境变量集中在 环境变量参考。启用文件系统隔离需要 SANDBOX_FILESYSTEM_ISOLATION_ENABLED=true 且 SANDBOX_FILESYSTEM_ISOLATION_BINARY 指向 Bubblewrap 可执行文件(Compose/Helm 默认 /usr/bin/bwrap)。Docker Compose 和 Helm 中的 sandbox-worker 默认使用 root + CAP_SYS_ADMIN + seccomp=unconfined 的安全配置;部署时若出现 bwrap 用户命名空间错误,见 故障排查。
参见:
- 工具与 Skills — 内置工具与沙箱边界
- 自定义 HTTP 与代码工具 — 配置代码工具
- 工作流节点 — 代码节点集成
- 环境变量参考 — Sandbox 环境变量
这篇文章对你有帮助吗?