- scripts/backfill-users.ts: 扫 PG users 表,对每人幂等调 orchestrator POST /users - orchestrator/package.json: 加 postgres 依赖 - .memory/project.md: Phase 3/4 完成状态,Phase 5 上线 checklist Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
140 lines
7.5 KiB
Markdown
140 lines
7.5 KiB
Markdown
# Lobe Sandbox Backend — 自托管沙箱后端
|
||
|
||
## What
|
||
|
||
替换 LobeChat 对 `market.lobehub.com` 云沙箱的依赖,改为公司 VPS(2.24.28.41)本地自托管的 **per-user Incus LXC** 工作区。
|
||
|
||
- **服务对象**:`ai.milejoy.com`(公司主)+ `lobehub.kang-kang.com`(个人备份,同 backend)
|
||
- **隔离粒度**:每个 LobeChat 用户一个 Incus 容器,跨对话共享文件系统(概念对标 ChatGPT Code Interpreter)
|
||
- **挂钩点**:better-auth `databaseHooks.user.create.after`(在 LobeChat 项目里改)
|
||
|
||
## Why 这个设计
|
||
|
||
- 三条路(a 自建 / b 走 LobeHub 白名单付费 / c 让员工个人账号 OAuth 授权),**a 省钱 + 数据不出公司 + 掌控力**
|
||
- 沙箱技术选 Incus LXC(不是 Docker / Firecracker / Daytona): 长命工作区 = 系统容器,公司已有 Incus 栈(hermes-box 同宿主),无新技术栈,CoW 省磁盘
|
||
- btrfs 存储驱动(不是 ZFS):Debian 13 内核原生支持,`apt install btrfs-progs` 零重启,CoW 效果等价
|
||
|
||
## 架构
|
||
|
||
```
|
||
LobeChat (Coolify)
|
||
↓ HTTP 内网(X-Sandbox-Secret HMAC)
|
||
Sandbox Orchestrator(Bun 写的 HTTP 服务,systemd 跑在 VPS 宿主机)
|
||
↓ incus CLI(unix socket)
|
||
Incus 宿主(VPS 2.24.28.41)
|
||
├── lobe-sandbox project
|
||
├── lobe-sandbox-pool(btrfs 200GiB,CoW)
|
||
├── sandbox-default profile(2GB RAM / 2 CPU / 10GiB disk)
|
||
└── sb-<userId> 容器 × N(默认 stopped,调用时起)
|
||
↓ exportFile
|
||
MinIO(192.168.2.221:9000)
|
||
```
|
||
|
||
## 生命周期
|
||
|
||
| 阶段 | 触发 | 动作 |
|
||
|---|---|---|
|
||
| 创建 | better-auth user.create | `incus copy lobe-sandbox-base → sb-<userId>`,不启动 |
|
||
| 激活 | 首次 tool 调用 | `incus start` 2-3 秒内可用 |
|
||
| 空闲 | 30min 无调用 | `incus stop`,rootfs 保留 |
|
||
| 删除 | 手动 / admin | 备份到 MinIO + `incus delete` |
|
||
|
||
## 部署目标
|
||
|
||
- VPS: `root@2.24.28.41`
|
||
- Orchestrator 端口: `127.0.0.1:8700`(内网,不对外暴露)
|
||
- Base 镜像: `lobe-sandbox-base`(Debian 13 + Python 3.13 + Node 20 + Bun + uv + 中文字体)
|
||
- Orchestrator 部署路径: `/opt/lobe-sandbox/orchestrator`(TBD)
|
||
- systemd unit: `sandbox-orchestrator.service`
|
||
|
||
## 关键决策 & 坑点
|
||
|
||
### 1. 存储池用 btrfs 不是 ZFS
|
||
原方案 ZFS,实发现 ZFS 在这台 Ubuntu 要 DKMS 编译,可能触发重启(用户规则:不频繁重启服务)。改用 btrfs,内核原生,一键 `apt install btrfs-progs` 零重启,CoW 效果一样。
|
||
|
||
### 2. ⚠️ UFW 挡了 incusbr0 的 DHCP/DNS(大坑)
|
||
**现象**:新容器用 `DHCP=true`,`DHCPDISCOVER` 发出去但 `No DHCPOFFERS received`,只拿到 IPv6 SLAAC。
|
||
|
||
**原因**:这台 VPS `iptables INPUT 策略=DROP`(UFW 默认)+ Docker 的 FORWARD chain。默认没放行 incusbr0 的 udp/67、udp/53、tcp/53,也没放 FORWARD。nftables 里 Incus 自己加的规则不生效(iptables 和 nft 都跑,都要通过)。
|
||
|
||
**绕开**:hermes-box 用**静态 IP**(`Address=10.146.223.10/24`),所以没触发这问题。
|
||
|
||
**修复**(已落盘到 `/etc/iptables/rules.v4`,通过 netfilter-persistent 持久):
|
||
```
|
||
iptables -I INPUT -i incusbr0 -p udp --dport 67 -j ACCEPT
|
||
iptables -I INPUT -i incusbr0 -p udp --dport 53 -j ACCEPT
|
||
iptables -I INPUT -i incusbr0 -p tcp --dport 53 -j ACCEPT
|
||
iptables -I FORWARD -i incusbr0 -j ACCEPT
|
||
iptables -I FORWARD -o incusbr0 -j ACCEPT
|
||
```
|
||
|
||
修复后 DHCP 立即可用(容器拿到 `10.146.223.248`)。本地 `scripts/host-init.sh` 里写了这段幂等实现,新环境部署自动修。
|
||
|
||
### 3. Python 版本
|
||
Debian 13 默认 Python 3.13(不是计划的 3.12)。不影响功能,更新。
|
||
|
||
### 4. 沙箱是 per-user 不是 per-topic
|
||
对话跨 topic 共享工作区,对齐 ChatGPT Code Interpreter 的心智模型。
|
||
|
||
### 5. 基座"什么都不装"原则
|
||
- 只装必需的运行时(Python/Node/Bun/uv)+ 系统工具(git/curl/build-essential/中文字体)
|
||
- **不预装** pandas/numpy/torch/playwright/ffmpeg/libreoffice
|
||
- LLM 按需 `uv pip install xxx` — 后续加本地 PyPI 缓存镜像让它快到秒级(尚未做,列在 TODO)
|
||
|
||
## 进度(2026-04-19 更新)
|
||
|
||
- ✅ Phase 0 Pre-flight(VPS 摸底:31GB RAM / 387GB 磁盘 / Incus 6.0.0 / hermes-box 已跑)
|
||
- ✅ Phase 1 宿主初始化(btrfs pool + project + profile + **UFW iptables 修复并持久化**)
|
||
- ✅ Phase 2 base 镜像 `lobe-sandbox-base` 已发布(500MiB,fingerprint `de8d9b6b73c4`)
|
||
- Debian 13 trixie + Python 3.13.5 + Node 20.20.2 + Bun 1.3.12 + uv 0.11.7 + git/ripgrep/fd/bat + 中文字体 + zh_CN locale
|
||
- **两个坑踩过并写进脚本**:
|
||
1. `INSTALL_DIR` 装 uv 无效,要用 `UV_INSTALL_DIR` 环境变量
|
||
2. `nohup bash -s <<EOF &` 脱不开 SSH,后续全改 `systemd-run --unit=xxx --slice=system.slice`
|
||
- ✅ Phase 3 Orchestrator(Bun + Hono,已部署 VPS systemd)
|
||
- 服务:`sandbox-orchestrator.service` 监听 `127.0.0.1:8700`
|
||
- 配置:`/etc/lobe-sandbox/orchestrator.env`(含 SANDBOX_ORCH_SECRET)
|
||
- 端到端验证:POST /users → executeCode → runCommand + uv pip install numpy → writeLocalFile → listLocalFiles 全 OK
|
||
- 已实现 13 工具(executeCode/runCommand/readLocalFile/writeLocalFile/editLocalFile/listLocalFiles/moveLocalFiles/renameLocalFile/searchLocalFiles/grepContent/globLocalFiles + ensureRunning 逻辑 + 30min idle reaper + HMAC auth + S3 export 占位)
|
||
- **长驻命令(getCommandOutput/killCommand)留到 v2**,当前 runCommand 是同步 2min 超时
|
||
- ✅ Phase 4 LobeChat 侧接入代码已提交(分支 `feat/self-hosted-sandbox`,commit `12b53e90b1`)
|
||
- `src/envs/app.ts` 新增 `SANDBOX_BACKEND_URL` + `SANDBOX_BACKEND_SECRET`
|
||
- `src/server/services/sandbox/localSandbox.ts` 新 LocalSandboxService
|
||
- `src/server/services/toolExecution/serverRuntimes/cloudSandbox.ts` env 分叉
|
||
- `src/libs/better-auth/define-config.ts` user.create.after 追加 provision(fire-and-forget)
|
||
- **注意**: main 分支 pre-commit 会跑全量 tsc 撑爆内存 + 碰到别人的 pre-existing TS 错误,所以必须在 feature branch 上 commit
|
||
- ⏸ Phase 5 待做(上线):
|
||
- [ ] 确定 LobeChat docker 容器访问 orchestrator 的网络路径(host.docker.internal / docker0 网关 172.17.0.1 / 绑 0.0.0.0 + UFW 限制)
|
||
- [ ] merge `feat/self-hosted-sandbox` 到 main(需先解决 main 上别人的 pre-existing TS 错误,或单独 deploy 这个 branch)
|
||
- [ ] Coolify 里 LobeChat 容器加 `SANDBOX_BACKEND_URL` + `SANDBOX_BACKEND_SECRET` 环境变量
|
||
- [ ] 重启 LobeChat(一次性,不要频繁重启)
|
||
- [ ] 跑 `scripts/backfill-users.ts` 给存量用户补建沙箱
|
||
- [ ] exportFile 接入:当前 `S3_ENDPOINT=192.168.2.221:9000` 是内网,VPS 够不着 → 改用 LobeChat 同机上的 RustFS(`lobemile2026/RustFS@2026MileSecure`)
|
||
- [ ] 端到端 smoke:用户 ai.milejoy.com 发"写个 Python 打印 hello" → 查 orchestrator 日志命中 → 不是 market.lobehub.com
|
||
|
||
## 关键路径速查
|
||
|
||
### 本地
|
||
- 项目根:`/Users/kangwan/Projects/business/20260418-lobe-sandbox-backend/`
|
||
- 宿主初始化脚本:`scripts/host-init.sh`
|
||
- base 镜像构建脚本:`images/base/build.sh`
|
||
|
||
### VPS(2.24.28.41)
|
||
- 构建日志:`/var/log/build-sandbox-base.log`(第一次,卡 UFW)
|
||
- 续跑日志:`/var/log/build-sandbox-base.log.2`(第二次,SSH 断开死)
|
||
- 构建脚本副本:`/root/build-sandbox-base.sh`
|
||
- Incus 数据:`/var/lib/incus/`
|
||
- btrfs pool 文件:`/var/lib/incus/disks/lobe-sandbox-pool.img`(200GiB 稀疏)
|
||
|
||
### Incus 操作(切到 lobe-sandbox project)
|
||
```bash
|
||
incus list --project lobe-sandbox
|
||
incus profile show sandbox-default --project lobe-sandbox
|
||
incus image list --project lobe-sandbox
|
||
```
|
||
|
||
## 规划中的 / 待决
|
||
- PyPI 本地缓存镜像(nginx 反代清华源)—— 让 `uv pip install` 秒级
|
||
- npm 本地缓存(Verdaccio)
|
||
- exportFile 对接 MinIO 的 presigned URL 上传流
|
||
- 用户删除的手动清理脚本 + 定时 GC(better-auth 没有 user.delete hook)
|