单容器,多身份
HTTP runner 与执行环境共处一个长期运行的容器。每个身份用自己的 UID;同一身份串行,不同身份并发。
专为单机服务器打造的轻量 Agent 沙箱。
一个容器,装下 Agent 的命令与文件。Runbox 给每个身份一个独立的 Linux 工作区: 上传输入、执行 Python / Node / Shell、取回产物。多个身份共享一个长期运行的容器, 各自拥有 UID、私有目录、配额和持久安装环境,不需要编排器或外部数据库。
不需要为每个用户新建容器,也不需要外部数据库或编排器。
$ docker compose up -d --build
$ curl -fsS http://localhost:8099/v1/exec \
-H "Authorization: Bearer $RUNBOX_API_KEY" \
-H "X-Runbox-Identity: alice" \
-H "Content-Type: application/json" \
-d '{"command":"echo hello from runbox"}'
{"exit_code":0,"stdout":"hello from runbox\n","files":[]}
$ find $HOME -maxdepth 2 -type d
/workspace/workspaces/<alice-key>
/workspace/workspaces/<alice-key>/files
/workspace/workspaces/<alice-key>/out
/workspace/workspaces/<alice-key>/.local
container-per-sandbox 和 microVM 都为每个沙箱准备独立运行时。面对互相敌对的租户这是正确的取舍, 但在单机上意味着每个用户多一个容器或虚拟机,往往还多一份 Python / LibreOffice。
| 每个沙箱一个容器 | 每个沙箱一台 microVM | Runbox | |
|---|---|---|---|
| 隔离单位 | 容器 | 虚拟机 | 单容器内 UID + 私有工作区 |
| 每个身份新建运行时 | 是 | 是 | 否 |
| 单机部署 | 控制面 + 多个容器 | KVM + 运行时 + 镜像 | 一个二进制、一个容器、一个卷 |
| 文件隔离 | 依赖卷或 API | 强 | 内置:工作区、配额、归档 |
| 适合 | 多机、强隔离 | 敌对租户 | 内部或半可信、单机 |
共享内核不是 microVM 边界。面对互相敌对的租户,请把 Runbox 放到 gVisor 或 microVM 下运行。
HTTP runner 与执行环境共处一个长期运行的容器。每个身份用自己的 UID;同一身份串行,不同身份并发。
私有工作区、工作区配额、可写目录之外的归档。符号链接不能用来读取或带出 runner 的文件。
HOME、XDG、pip 和 npm 前缀都在工作区里。装过的包和中间文件跨调用保留,计入配额。
上传边读边落盘,不把整个请求体读进内存;下载支持 Range 和 HEAD,适合大产物。
批量上传多个文件;按相对路径下载任意中间文件,或把目录打成 tar 一次取回,不需要为每个文件生成 ID。
命令长度、输入数量、stdout、产物数量与大小、工作区配额、归档配额、超时、并发和保留期都有边界。
身份、UID 和元数据写入一个 SQLite 文件,随数据卷持久化;不跟踪每个文件。
只读根文件系统、丢弃能力、no-new-privileges、tini 作为 PID 1;缺少所需能力时拒绝启动。
/ready 就绪检查、/metrics Prometheus 指标、结构化文本或 JSON 请求日志。
每个请求带一个身份。runner 把命令 cwd 指向该身份的目录,在 exec 前切换 UID。
命令写 out/report.csv 时,落点由 cwd、HOME 和目录权限共同决定。
RUNBOX_ROOT/
├── workspaces/<identity-key>/ 0750,属主为该身份的 UID
│ ├── files/ 上传的输入
│ ├── out/ 执行产物
│ ├── .local/ .npm-global/ 持久的 pip / npm 安装
│ ├── .cache/ .config/ .tmp/ 持久的 XDG 状态与临时文件
│ └── ...
├── keep/<identity-key>/ 不可变交付,属主为 runner
└── state/
├── runbox.db SQLite:身份、UID、元数据
├── runbox.db-wal
└── runbox.db-shm
git clone https://github.com/xy200303/runbox.git
cd runbox
cp .env.example .env
# 写入足够长的 RUNBOX_API_KEY
docker compose up -d --build
curl -fsS http://localhost:8099/health
curl -fsS http://localhost:8099/v1/exec \
-H "Authorization: Bearer $RUNBOX_API_KEY" \
-H "X-Runbox-Namespace: team-a" \
-H "X-Runbox-Principal: user-42" \
-H "Content-Type: application/json" \
-d '{"command":"mkdir -p out && echo hello > out/report.txt"}'
产物返回 file_id;任意中间文件用 GET /v1/path?path=... 下载,目录用 /v1/archive 打包。
go get github.com/xy200303/runbox/sdk/go
client, _ := sandbox.NewClient(sandbox.ClientOptions{
BaseURL: "http://localhost:8099",
APIKey: os.Getenv("RUNBOX_API_KEY"),
Identity: sandbox.ScopedIdentity("team-a", "user-42"),
})
result, _ := client.Run(ctx, sandbox.ExecRequest{
Command: "python3 -c 'print(6 * 7)'",
})
pip install runbox-sdk
from runbox import Client, Identity
client = Client(
"http://localhost:8099",
api_key="...",
identity=Identity(namespace="team-a", principal="user-42"),
)
print(client.exec("echo hello")["stdout"])
npm install runbox-sdk
import { Client } from 'runbox-sdk'
const client = new Client({
baseUrl: 'http://localhost:8099',
apiKey: process.env.RUNBOX_API_KEY,
identity: { namespace: 'team-a', principal: 'user-42' },
})
console.log((await client.exec('echo hello')).stdout)
把数据根目录挂载为卷即可:workspaces/、keep/ 和 state/runbox.db 都在里面。
docker run -d --name runbox \
-p 8099:8099 \
-e RUNBOX_API_KEY=... \
-v runbox-data:/workspace \
ghcr.io/xy200303/runbox:latest
只读根、丢弃全部能力后再补回所有权相关能力、no-new-privileges、资源上限。启动时缺少能力直接失败。
cap_drop: [ALL]
cap_add: [CHOWN, FOWNER, SETUID,
SETGID, DAC_OVERRIDE, KILL]
security_opt:
- no-new-privileges:true
不会。所有身份共享一个长期运行的容器,每个身份在里面拥有一个 UID 和私有工作区。这正是它能单机部署的原因。
不需要。任意文件都可以通过相对路径下载,例如 GET /v1/path?path=tmp/step2/data.csv;目录用 GET /v1/archive?path=out 打成 tar。file ID 只用于上传文件和执行产物。
不能。身份不是 root,根文件系统只读。系统级依赖应该在镜像构建时预装;pip/npm 安装落在各自工作区。
不能把它当作强多租户边界。Runbox 使用容器 namespace/cgroup 与按 UID 的文件权限,内核是共享的。面对敌对租户,请放到 gVisor 或 microVM 下运行。
形态相近:一个容器,每次执行切 UID。区别是 Runbox 的身份和 UID 持久化,工作区跨调用保留,文件可以按路径或归档下载,面向独立应用而不是 Dify 内部代码节点。
备份 workspaces/(如果中间状态重要)、keep/ 和 state/runbox.db。SQLite 在 WAL 模式,建议停容器或用 SQLite 备份接口复制。
从 Docker 或 SDK 开始,十分钟内跑通第一条命令。