Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,38 @@ jobs:
push: true
platforms: linux/amd64,linux/arm64
file: docker/Dockerfile.sandbox
target: sandbox
context: .
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha,scope=build-sandbox
cache-to: type=gha,scope=build-sandbox,mode=max

# Cube builds templates straight from the image and gates the build on
# GET :49983/health, so it needs the variant with envd baked in.
- name: Docker meta (Cube variant)
id: meta-cube
uses: docker/metadata-action@v6
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/weknora-sandbox
flavor: |
suffix=-cube,onlatest=true

# amd64 only: cubesandbox-base, the source of the envd binary, publishes
# no arm64 image.
- name: Build sandbox Cube Image
uses: docker/build-push-action@v7
with:
push: true
platforms: linux/amd64
file: docker/Dockerfile.sandbox
target: cube
context: .
labels: ${{ steps.meta-cube.outputs.labels }}
tags: ${{ steps.meta-cube.outputs.tags }}
cache-from: type=gha,scope=build-sandbox-cube
cache-to: type=gha,scope=build-sandbox-cube,mode=max

build-app:
strategy:
matrix:
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ services:
build:
context: .
dockerfile: docker/Dockerfile.sandbox
target: sandbox
profiles:
- full
command: ["true"]
Expand Down
54 changes: 48 additions & 6 deletions docker/Dockerfile.sandbox
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
# WeKnora Sandbox Image
# Pre-built environment for executing agent skill scripts in Docker sandbox
# Multi-stage build, minimal dependencies
#
# Two targets ship from this file:
# runtime (default) - the plain environment. Used by the Docker backend and
# as the base image of E2B templates, whose builder
# injects its own envd.
# cube - the same environment plus Cube's envd daemon. Cube
# builds templates straight from the image and probes
# :49983/health, so an image without envd can only ever
# fail. See docs/sandbox-cluster.md.

# Stage 1: Get Node.js binaries
FROM node:20-slim AS node-base

# Stage 2: Final image
FROM python:3.11-slim
# Stage 2: Runtime image
FROM python:3.11-slim AS runtime

# Copy Node.js from node image (avoids NodeSource install overhead)
COPY --from=node-base /usr/local/bin/node /usr/local/bin/
Expand All @@ -22,9 +31,42 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
# Note: Current preloaded skills only use Python stdlib
# Add packages here when skills actually need them:

# Create non-root user (UID 1000) for sandbox execution
RUN groupadd -g 1000 sandbox && \
useradd -u 1000 -g sandbox -m -s /bin/bash sandbox
# Create non-root user (UID 1000) for sandbox execution. The account is named
# "user" because that is the account name E2B templates are expected to expose
# and the one WeKnora names when running scripts and file operations against an
# E2B-protocol backend; the Docker backend addresses it numerically as
# 1000:1000, so both paths land on the same account.
RUN groupadd -g 1000 user && \
useradd -u 1000 -g user -m -s /bin/bash user

WORKDIR /workspace
USER sandbox
USER user

# Stage 3: Cube template image
#
# envd is what Cube talks to for everything — the readiness probe that decides
# whether a template build succeeds, plus every later exec and file call. It
# runs as root and drops to the account named in each request, which is why
# this variant does not keep the runtime's USER user.
#
# This target is linux/amd64 only: cubesandbox-base publishes no arm64 image,
# which matches Cube itself (PVM is x86_64-only).
FROM ghcr.io/tencentcloud/cubesandbox-base:2026.16 AS cube-base

FROM runtime AS cube

USER root
COPY --from=cube-base /usr/bin/envd /usr/bin/envd
COPY --from=cube-base /usr/local/bin/cube-entrypoint.sh /usr/local/bin/cube-entrypoint.sh

EXPOSE 49983
# The entrypoint backgrounds envd and then waits on it, since this image
# defines no CMD of its own.
ENTRYPOINT ["/usr/local/bin/cube-entrypoint.sh"]

# Stage 4: default target
#
# Docker builds the last stage when none is named, and that must stay the plain
# runtime: a bare `docker build -f docker/Dockerfile.sandbox .` is expected to
# produce the image the Docker backend runs, not the Cube variant.
FROM runtime AS sandbox
79 changes: 74 additions & 5 deletions docs/sandbox-cluster.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,91 @@
- Node.js 20、npm 与 npx;
- jq 及基础 Shell 工具;
- `/workspace` 工作目录;
- UID 1000 的非 root `sandbox` 用户
- UID 1000 的非 root `user` 账号(E2B 模板约定的账号名,WeKnora 以它执行脚本与文件操作)

生产环境应使用与 WeKnora 相同的版本标签,不建议长期指向 `latest`。Skills 新增系统依赖时,应先更新标准镜像并重新注册模板,再切换集群的默认模板 ID。

### 两个镜像变体

`docker/Dockerfile.sandbox` 产出两个 target,内容相同、入口不同:

| 变体 | 标签 | 用途 |
| --- | --- | --- |
| `sandbox`(默认) | `wechatopenai/weknora-sandbox:<版本>` | Docker 后端直接执行脚本;同时作为 E2B 模板的基础镜像 |
| `cube` | `wechatopenai/weknora-sandbox:<版本>-cube` | CubeSandbox 模板 |

区别在于 Cube 变体额外注入了 envd。Cube 直接把 OCI 镜像变成模板,并以 `GET :49983/health` 探活,这个端点只有 envd 提供;不带 envd 的镜像建模板必然以 `connection refused` 失败。E2B 不需要这个变体,因为它的构建流程会自行注入 envd;Docker 后端则完全不需要 envd。详见 [Cube 自带镜像接入](https://cubesandbox.com/zh/guide/tutorials/bring-your-own-image.html)。

Cube 变体只发布 linux/amd64——envd 的来源镜像 `cubesandbox-base` 没有 arm64,Cube 自身的 PVM 形态也只支持 x86_64。变体内 envd 以 root 运行,脚本仍按请求指定的账号执行,落在同一个 uid 1000 的 `user` 上。

## CubeSandbox

1. 按 [CubeSandbox Quick Start](https://github.com/TencentCloud/CubeSandbox/blob/master/docs/guide/quickstart.md) 完成控制面、计算节点、CubeProxy 和域名解析配置。生产环境还需要按官方文档完成鉴权、TLS、网络策略和多节点部署。
### 先选部署形态

| 形态 | 适用 | 硬性前提 |
| --- | --- | --- |
| 裸金属 / 物理机 | 已有可用 KVM 的机器 | `/dev/kvm` 可读写;root 权限 |
| PVM | 云厂商屏蔽了嵌套虚拟化、`/dev/kvm` 不可用 | 仅 x86_64;需安装 PVM 宿主机内核并重启;ARM64 不支持 |
| Kubernetes(preview) | 已有集群、要多节点 | K8s 1.24+;计算节点打标签;计算节点非裸金属时需开启 PVM bootstrap |

三种形态共同的前提:

- glibc ≥ 2.31(官方二进制基于 Ubuntu 20.04 构建);
- `/data/cubelet` 挂载 XFS,且开启 reflink(快照的 Copy-on-Write 依赖它)。Ubuntu/Debian 默认 ext4,需要单独准备分区或 loop 设备;
- 至少 50 GB 可用磁盘(要做多个模板则建议 200 GB 以上)、内存 ≥ 8 GB;
- 内核支持 eBPF 且 `/sys/fs/bpf` 已挂载为 bpffs(Cubelet 的网络运行时依赖);
- 具备 `resolvectl` 或 NetworkManager(安装脚本据此配置 `cube.app` 的 DNS 解析);
- Docker 可用(MySQL/Redis 以 Docker Compose 运行)。

安装脚本会把 CubeMaster、Cubelet、CubeShim 作为宿主机进程运行,因此**不能在没有 systemd 的容器化环境里部署**。CI 容器、无 init 系统的开发机请改用 K8s 形态或另找一台主机。

### 制作模板并对接 WeKnora

1. 按 [CubeSandbox Quick Start](https://github.com/TencentCloud/CubeSandbox/blob/master/docs/zh/guide/quickstart.md) 完成控制面、计算节点、CubeProxy 与域名解析。生产环境还需按官方文档完成鉴权、TLS、网络策略与多节点部署。
2. 在 WeKnora 的空间设置中填写 CubeAPI、CubeProxy、sandbox domain 和可选 API Key。若这些端点位于 RFC1918/loopback 网络,显式打开“允许访问私网集群地址”。
3. 点击“连接并继续”。WeKnora 先验证控制面地址与凭据,通过后才进入模板步骤并调用集群模板列表;如果不存在名为 `weknora` 的标准模板,会从 `wechatopenai/weknora-sandbox:latest` 发起一次构建。
3. 点击“连接并继续”。WeKnora 先验证控制面地址与凭据,通过后才进入模板步骤并调用集群模板列表;如果集群里没有可用的 WeKnora 标准模板,会从 `wechatopenai/weknora-sandbox:latest-cube` 发起一次构建。已有模板按名称 `weknora` 或镜像识别,因此即使集群没有给模板登记别名也不会重复创建;已有模板构建失败时,走的是原地重建而不是新建一个
4. 模板构建状态会自动刷新。状态变为 `READY` 后才可选择并进入运行配置;界面显示模板名称、状态和版本,配置内部才保存该集群自己的 `template_id`。

模板镜像必须提供 uid 1000 的 `user` 账号:WeKnora 以该账号执行脚本与文件操作。写权限只保证在 `/workspace/output` 与 `/workspace/input` 下。

多实例 WeKnora 必须配置 Redis,以共享 session 到 sandbox 的绑定。只有单实例开发环境才应使用内存绑定。

## E2B
### 验证

界面上的“连接验证”只覆盖控制面,“完整验证”会真实创建、执行并销毁一个沙箱。要覆盖 WeKnora 实际依赖的全部语义(会话内状态保持、shell_exec 复用同一沙箱、附件暂存、产物收集、执行超时),在能访问集群的机器上跑一致性测试:

```bash
CUBE_API_URL=http://127.0.0.1:33000 \
CUBE_PROXY_URL=http://127.0.0.1:80 \
CUBE_TEMPLATE_ID=<模板 ID> \
go test -tags=integration ./internal/sandbox -run Integration -count=1 -v
```

测试结束会归还自己创建的沙箱;跑完用 `cubemastercli` 确认没有残留实例。

### 排障

| 现象 | 排查方向 |
| --- | --- |
| 连接验证失败 | CubeAPI 地址是否是控制面端口(不是 Dashboard 端口);私网地址是否已打开“允许访问私网集群地址” |
| 连接通过但执行报数据面错误 | CubeProxy 地址与 sandbox domain 是否与集群 `CUBE_API_SANDBOX_DOMAIN` 一致;Proxy 是否对 WeKnora 可达 |
| 模板长期停在构建中 | 镜像体积与网络;用 `cubemastercli tpl watch --job-id <id>` 看真实进度 |
| 模板构建失败 | 卡片上会显示集群返回的失败原因;点「刷新模板」会就地重建同一个模板,不会新建一个 |
| 失败原因是 `Get "http://<IP>:49983/health": connect: connection refused` | 模板镜像里没有 envd。确认建模板用的是 `-cube` 变体镜像;若已是该变体,再查 Cube 沙箱网段(默认 `192.168.0.0/18`)是否与物理内网冲突 |
| 沙箱能创建但脚本报权限错误 | 模板缺少 `user` 账号,或脚本写到了 `/workspace` 根目录 |
| 会话重连后状态丢失 | 多副本部署是否配置了 Redis 绑定存储;沙箱是否已被空闲 TTL 回收 |

### 已知限制

- K8s 部署仍是 preview:计算节点资源紧张时 Pod 可能被误驱逐,计算面升级会重建 Big Pod 并中断存量沙箱。
- 官方镜像的 Multi-Arch 覆盖尚不完整,ARM64 环境需自行构建模板镜像。
- 暂不支持 GPU 直通。

其它 E2B 兼容后端(含容器隔离的 Kubernetes 实现)与协议层的统一方案见 [沙箱协议接入说明](./sandbox-protocol.md)。

## E2B 及其它 E2B 兼容实现

E2B 官方托管服务和自建 E2B Infrastructure 都可接入。填写 API Key 后先执行“连接并继续”,流程与 Cube 相同:验证连接后列出账号可见模板,缺少 `weknora` 时通过 E2B Template API 从标准镜像启动后台构建。自建部署还需填写 API URL 和 sandbox domain;E2B 上游通过 Terraform 提供 AWS、GCP 等部署方式,具体以 [E2B self-hosting guide](https://github.com/e2b-dev/infra/blob/main/self-host.md) 和 [E2B Template 文档](https://e2b.dev/docs/template/quickstart) 为准。
E2B 官方托管服务、自建 E2B Infrastructure,以及任意实现 E2B 协议的控制面(例如 Kubernetes 上以容器隔离的 Agent-Sandbox)都通过同一个 E2B 配置接入;自建集群通常还要填写 `proxy_url` 数据面网关,详见 [沙箱协议接入说明](./sandbox-protocol.md)。填写 API Key 后先执行“连接并继续”,流程与 Cube 相同:验证连接后列出账号可见模板,缺少 `weknora` 时通过 E2B Template API 从标准镜像启动后台构建。自建部署还需填写 API URL 和 sandbox domain;E2B 上游通过 Terraform 提供 AWS、GCP 等部署方式,具体以 [E2B self-hosting guide](https://github.com/e2b-dev/infra/blob/main/self-host.md) 和 [E2B Template 文档](https://e2b.dev/docs/template/quickstart) 为准。

## 在设置页面完成接入

Expand Down
Loading